<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="generator" content="rustdoc">
    <title>Flutter State Management with Bloc for PineTime Companion App</title>

    
    <!-- Begin scripts/articles/*-header.html: Article Header for Custom Markdown files processed by rustdoc, like chip8.md -->
<meta property="og:title" 
    content="Flutter State Management with Bloc for PineTime Companion App" 
    data-rh="true">
<meta property="og:description" 
    content="How we manage state with the Bloc Library in the Flutter Companion App (Android and iOS) for PineTime Smart Watch" 
    data-rh="true">
<meta property="og:image" 
    content="https://lupyuen.github.io/images/bloc-title.jpg">
<meta property="og:type" 
    content="article" data-rh="true">
<!-- End scripts/articles/*-header.html -->
<!-- Begin scripts/rustdoc-header.html: Header for Custom Markdown files processed by rustdoc, like chip8.md -->
<link rel="alternate" type="application/rss+xml" title="RSS Feed for lupyuen" href="/rss.xml" />
<link rel="stylesheet" type="text/css" href="../normalize.css">
<link rel="stylesheet" type="text/css" href="../rustdoc.css" id="mainThemeStyle">
<link rel="stylesheet" type="text/css" href="../dark.css">
<link rel="stylesheet" type="text/css" href="../light.css" id="themeStyle">
<script src="../storage.js"></script><noscript>
<link rel="stylesheet" href="../noscript.css"></noscript>
<link rel="shortcut icon" href="../favicon.ico">
<style type="text/css">
    #crate-search {
        background-image: url("../down-arrow.svg");
    }
    a {
        color: #77d;
    }
</style>
<!-- End scripts/rustdoc-header.html -->


</head>
<body class="rustdoc">
    <!--[if lte IE 8]>
    <div class="warning">
        This old browser is unsupported and will most likely display funky
        things.
    </div>
    <![endif]-->

        <!-- Begin scripts/rustdoc-before.html: Pre-HTML for Custom Markdown files processed by rustdoc, like chip8.md -->

    <!-- Begin Theme Picker -->
    <div class="theme-picker" style="left: 0"><button id="theme-picker" aria-label="Pick another theme!"><img src="../brush.svg"
        width="18" alt="Pick another theme!"></button>
        <div id="theme-choices"></div>
    </div>
    <script src="../theme.js"></script>
    <!-- Theme Picker -->

    <!-- End scripts/rustdoc-before.html -->
    

    <h1 class="title">Flutter State Management with Bloc for PineTime Companion App</h1>
    <nav id="TOC"><ul>
<li><a href="#why-manage-state">1 Why Manage State?</a><ul></ul></li>
<li><a href="#flutter-widgets-for-pinetime">2 Flutter Widgets for PineTime</a><ul>
<li><a href="#device-firmware-widget">2.1 Device Firmware Widget</a><ul></ul></li>
<li><a href="#device-summary-widget">2.2 Device Summary Widget</a><ul></ul></li>
<li><a href="#device-widget">2.3 Device Widget</a><ul></ul></li></ul></li>
<li><a href="#pinetime-data-model">3 PineTime Data Model</a><ul></ul></li>
<li><a href="#send-bluetooth-le-request-to-pinetime">4 Send Bluetooth LE Request to PineTime</a><ul>
<li><a href="#connect-to-pinetime">4.1 Connect to PineTime</a><ul></ul></li>
<li><a href="#discover-gatt-services">4.2 Discover GATT Services</a><ul></ul></li>
<li><a href="#find-gatt-characteristic">4.3 Find GATT Characteristic</a><ul></ul></li>
<li><a href="#write-to-gatt-characteristic">4.4 Write to GATT Characteristic</a><ul></ul></li></ul></li>
<li><a href="#handle-bluetooth-le-response-from-pinetime">5 Handle Bluetooth LE Response from PineTime</a><ul></ul></li>
<li><a href="#decode-cbor-response-from-pinetime">6 Decode CBOR Response from PineTime</a><ul></ul></li>
<li><a href="#business-logic-states-and-events-in-bloc">7 Business Logic, States and Events in Bloc</a><ul>
<li><a href="#business-logic">7.1 Business Logic</a><ul></ul></li>
<li><a href="#trigger-events">7.2 Trigger Events</a><ul></ul></li>
<li><a href="#load-data">7.3 Load Data</a><ul></ul></li>
<li><a href="#update-widgets">7.4 Update Widgets</a><ul></ul></li>
<li><a href="#multiple-blocs">7.5 Multiple Blocs</a><ul></ul></li>
<li><a href="#tracing-states-and-events">7.6 Tracing States and Events</a><ul></ul></li></ul></li>
<li><a href="#build-and-run-app">8 Build and Run App</a><ul>
<li><a href="#download-source-code">8.1 Download Source Code</a><ul></ul></li>
<li><a href="#debug-flutter-app">8.2 Debug Flutter App</a><ul></ul></li>
<li><a href="#sign-flutter-app-for-ios">8.3 Sign Flutter App for iOS</a><ul></ul></li></ul></li>
<li><a href="#whats-next">9 What's Next</a><ul></ul></li>
<li><a href="#further-reading">10 Further Reading</a><ul></ul></li>
<li><a href="#sample-android-debug-log">11 Sample Android Debug Log</a><ul></ul></li>
<li><a href="#sample-ios-debug-log">12 Sample iOS Debug Log</a><ul></ul></li></ul></nav><p><img src="https://lupyuen.github.io/images/bloc-title.jpg" alt="PineTime Companion App running on an Android phone, fetching the firmware versions from PineTime Smart Watch wirelessly" /></p>
<p><em>PineTime Companion App running on an Android phone, fetching the firmware versions from PineTime Smart Watch wirelessly</em></p>
<p><a href="https://flutter.dev/"><strong>Flutter is awesome</strong></a> for creating mobile apps for Android and iOS... The programming language is modern <a href="https://dart.dev/"><strong>(Dart)</strong></a>, the debugging tools are excellent <a href="https://flutter.dev/docs/development/tools/vs-code"><strong>(VSCode and Dart DevTools)</strong></a>.</p>
<p>That's why we have selected Flutter for creating the open source <strong><a href="https://github.com/lupyuen/pinetime-companion/tree/bloc">Android and iOS Companion App</a></strong> for <strong><a href="https://lupyuen.github.io/pinetime-rust-mynewt/articles/pinetime">PineTime Smart Watch</a></strong>: For updating firmware, syncing date/time, pushing mobile notifications, controlling our smart home gadgets, ... and so much more!</p>
<p>In the previous article <em><a href="https://lupyuen.github.io/pinetime-rust-mynewt/articles/companion">&quot;Convert Go to Flutter and Dart for PineTime Companion App&quot;</a></em> we have built a technically functional (but barely human) app that sends Bluetooth Low Energy commands to PineTime. </p>
<p>Today we'll recode the app so that it's more human-friendly, like this...</p>
<ul>
<li>
<p><a href="https://youtu.be/VUYqcD8VGzE">Watch on YouTube</a></p>
</li>
<li>
<p><a href="https://github.com/lupyuen/pinetime-companion/releases/download/v1.0.0/companion-version.mp4">Download the video</a></p>
</li>
</ul>
<p>Read on to learn how we do this with the <strong><a href="https://bloclibrary.dev/">Bloc Library for State Management</a></strong>...</p>
<h1 id="why-manage-state" class="section-header"><a href="#why-manage-state">1 Why Manage State?</a></h1>
<p><em>It's easy to build a mobile app for Android and iOS with Flutter... What could go wrong?</em></p>
<p>A mobile app is a simple thing... It just reacts to our tapping and updates the display...</p>
<p><img src="https://lupyuen.github.io/images/bloc-state1.png" alt="Mobile app reacts to our tapping and updates the display" /></p>
<p><em>Is it really so simple?</em></p>
<p>Nope! Behind the scenes, the app could be calling some REST API on the web. Or talking to Bluetooth gadgets like PineTime...</p>
<p><img src="https://lupyuen.github.io/images/bloc-state2.png" alt="Mobile app calls REST API and talks to Bluetooth gadgets" /></p>
<p><em>What happens when the app loses track of its state?</em></p>
<p>Things can become really complicated...</p>
<p><img src="https://lupyuen.github.io/images/bloc-state3.png" alt="Mobile app gets complicated" /></p>
<p>There's a good way to handle this messy state in Flutter apps... <strong><a href="https://bloclibrary.dev/">Bloc Library for State Management</a></strong>! </p>
<p>Read on to learn how.</p>
<p><em>(If you're familiar with React Redux: Yep Bloc sounds a lot like React Redux, because they are both State Management Frameworks)</em></p>
<h1 id="flutter-widgets-for-pinetime" class="section-header"><a href="#flutter-widgets-for-pinetime">2 Flutter Widgets for PineTime</a></h1>
<p>Our PineTime Companion App calls the <strong><a href="https://bloclibrary.dev/">Bloc Library</a></strong> to connect our Flutter Widgets with our application data and processing logic. </p>
<p>Let's look at three Flutter Widgets that we have created for the app...</p>
<ol>
<li>
<p><strong>Device Firmware Widget (Stateless):</strong> Shows firmware version numbers</p>
</li>
<li>
<p><strong>Device Summary Widget (Stateless):</strong> Summarises the PineTime info</p>
</li>
<li>
<p><strong>Device Widget (Stateful):</strong> The entire PineTime Companion screen</p>
</li>
</ol>
<p>We'll learn why the widgets are Stateless / Stateful in a while.</p>
<h2 id="device-firmware-widget" class="section-header"><a href="#device-firmware-widget">2.1 Device Firmware Widget</a></h2>
<p>Our Flutter App talks to PineTime over Bluetooth LE (Low Energy) to fetch the firmware version numbers and display them. Here's how it looks...</p>
<p><img src="https://lupyuen.github.io/images/bloc-widgets1.png" alt="Device Firmware Widget" /></p>
<p><em>(PineTime contains two firmware images: Active and Standby. If the Active Firmware fails to start, PineTime rolls back to the Standby Firmware)</em></p>
<p>The <strong>Device Firmware Widget</strong> that displays the firmware version numbers is really simple: <a href="https://github.com/lupyuen/pinetime-companion/blob/bloc/lib/widgets/device_firmware.dart"><code>widgets/device_firmware.dart</code></a></p>
<pre><code class="language-dart">/// Widget to display firmware versions fetched from PineTime
class DeviceFirmware extends StatelessWidget {
  final String activeFirmwareVersion;   //  Version number of firmware that's running on PineTime (e.g. '1.0.0')
  final String standbyFirmwareVersion;  //  Version number of firmware that's in external flash memory (e.g. '1.1.0')

  /// Construct the widget with the active and standby firmware version numbers
  DeviceFirmware({
    Key key,
    this.activeFirmwareVersion,
    this.standbyFirmwareVersion
  }) : super(key: key);

  /// Render the widget UI with two lines of text
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        //  Show active firmware version number
        Text(
          'Active Firmware: $activeFirmwareVersion',
          style: TextStyle(
            fontSize: 16,
            fontWeight: FontWeight.w100,
            color: Colors.white,
          ),
        ),
        //  Show standby firmware version number
        Text(
          'Standby Firmware: $standbyFirmwareVersion',
          style: TextStyle(
            fontSize: 16,
            fontWeight: FontWeight.w100,
            color: Colors.white,
          ),
        )
      ],
    );
  }
}
</code></pre>
<p><code>DeviceFirmware</code> contains two fields <code>activeFirmwareVersion</code> and <code>standbyFirmwareVersion</code>, that store the version numbers of the Active and Standby Firmware on PineTime.</p>
<p><code>DeviceFirmware</code> is a <a href="https://flutter.dev/docs/development/ui/interactive"><strong>Stateless Widget</strong></a> because its State (<code>activeFirmwareVersion</code> and <code>standbyFirmwareVersion</code>) doesn't change.</p>
<p><em>What happens if PineTime gets updated with new firmware?</em></p>
<p>Our Flutter App shall create a new instance of <code>DeviceFirmware</code> with new values for <code>activeFirmwareVersion</code> and <code>standbyFirmwareVersion</code>.</p>
<p>That's why the Device Firmware widget will never change its State... Though the widget may get replaced altogether.</p>
<h2 id="device-summary-widget" class="section-header"><a href="#device-summary-widget">2.2 Device Summary Widget</a></h2>
<p>The Device Firmware widget we've seen is wrapped into a <strong>Device Summary Widget</strong> like this...</p>
<p><img src="https://lupyuen.github.io/images/bloc-widgets2.png" alt="Device Summary Widget" /></p>
<p><code>DeviceSummary</code> is defined in <a href="https://github.com/lupyuen/pinetime-companion/blob/bloc/lib/widgets/device_summary.dart"><code>widgets/device_summary.dart</code></a>...</p>
<pre><code class="language-dart">/// Widget to display PineTime summary
class DeviceSummary extends StatelessWidget {
  /// Data Model that contains PineTime info and Bluetooth device
  final model.Device device;

  /// Render the PineTime summary
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ...,
        //  Construct a DeviceFirmware Widget to show the firmware versions
        DeviceFirmware(
          activeFirmwareVersion: device.activeFirmwareVersion,
          standbyFirmwareVersion: device.standbyFirmwareVersion,
        )
</code></pre>
<p><em>What's with the Data Model named <code>Device</code>?</em></p>
<pre><code class="language-dart">/// Data Model that contains PineTime info and Bluetooth device
final model.Device device;
</code></pre>
<p>To render the Device Summary, this widget needs to know everything about our PineTime gadget... That's why the widget keeps a copy of the PineTime info inside the <strong>Data Model</strong> named <code>Device</code>.</p>
<p>Note that the Device Summary Widget passes two fields from the <code>Device</code> Data Model to the Device Firmware Widget: <code>activeFirmwareVersion</code> and <code>standbyFirmwareVersion</code>.</p>
<p><code>DeviceSummary</code> is another <strong>Stateless Widget</strong> that doesn't change its State (i.e. the <code>Device</code> Data Model). If the device info changes, our app creates a new <code>DeviceSummary</code> widget to replace the old one.</p>
<p>The Data Model is a core concept in the Bloc Library. More about this later.</p>
<h2 id="device-widget" class="section-header"><a href="#device-widget">2.3 Device Widget</a></h2>
<p>The Device Summary Widget above is wrapped into a <strong>Device Widget</strong> that renders the entire screen...</p>
<p><img src="https://lupyuen.github.io/images/bloc-widgets3.png" alt="Device Widget" /></p>
<p>Device Widget is a <a href="(https://flutter.dev/docs/development/ui/interactive)"><strong>Stateful Widget</strong></a> that has some interesting code inside: <a href="https://github.com/lupyuen/pinetime-companion/blob/bloc/lib/widgets/device.dart"><code>widgets/device.dart</code></a></p>
<pre><code class="language-dart">/// Widget for the PineTime Companion screen
class Device extends StatefulWidget {
  /// Construct the Stateful Widget with an initial state
  @override
  State&lt;Device&gt; createState() =&gt; _DeviceState();
}

/// Implement the Stateful Widget for the PineTime Companion screen
class _DeviceState extends State&lt;Device&gt; {
  /// Render the PineTime Companion screen
  @override
  Widget build(BuildContext context) {
    //  Render the screen with Button Bar above, followed by the Body
    return Scaffold(
      //  Button Bar for the screen (omitted)
      appBar: ...,

      //  Body for the screen
      body: 
        ...
        //  Construct a BlocConsumer to listen for updates to the state and rebuild the widget
        BlocConsumer&lt;DeviceBloc, DeviceState&gt;(
          //  Listen for updates to the state
          listener: ...,

          //  Rebuild the widget when the state has been updated
          builder: (context, state) {
            //  When we have loaded the device info...
            if (state is DeviceLoadSuccess) {...
              //  Get the device info from the new state
              final device = state.device;

              //  Construct the Device Summary with the device info
              return 
                ...
                DeviceSummary(
                  device: device,
                ),
                ...
</code></pre>
<p><em>Why do we need <code>createState()</code> in the Device Widget?</em></p>
<p>Device Widget is a <a href="https://flutter.dev/docs/development/ui/interactive"><strong>Stateful Widget</strong></a>, so it needs to be created with an initial state, like this...</p>
<pre><code class="language-dart">/// Widget for the PineTime Companion screen
class Device extends StatefulWidget {
  /// Construct the Stateful Widget with an initial state
  @override
  State&lt;Device&gt; createState() =&gt; _DeviceState();
</code></pre>
<p><em>Why is the Device Widget Stateful, unlike the other Widgets?</em></p>
<p>Because the Device Widget will magically transform itself when something happens!</p>
<p>The code below says that the Device Widget will rebuild its Device Summary Widget when the State has changed to <code>DeviceLoadSuccess</code>...</p>
<pre><code class="language-dart">//  Rebuild the widget when we receive an event
builder: (context, state) {
  //  When we have loaded the device info...
  if (state is DeviceLoadSuccess) {...
    //  Get the device info from the new state
    final device = state.device;

    //  Construct the Device Summary with the device info
    return 
      ...
      DeviceSummary(
        device: device,
      ),
      ...
</code></pre>
<p>The above <a href="https://bloclibrary.dev/#/flutterbloccoreconcepts?id=blocbuilder"><strong>Bloc Widget Builder</strong></a> (exposed by <a href="https://bloclibrary.dev/#/flutterbloccoreconcepts?id=blocconsumer"><code>BlocConsumer</code></a>) takes the updated <code>Device</code> Data Model from the new State, and creates a new Device Summary Widget...</p>
<p><img src="https://lupyuen.github.io/images/bloc-builder.png" alt="Rebuilding the Device Summary Widget on state updates" /></p>
<p>This explains why the Device Widget is Stateful while the Device Summary Widget (and Device Firmware Widget) is Stateless... Because Device Widget will replace the Device Summary Widget when there are updates.</p>
<p><em>How do we trigger the <code>DeviceLoadSuccess</code> State?</em></p>
<p>This State is triggered when we have loaded the device info from PineTime over Bluetooth LE.</p>
<p>That's how widgets get updated in Bloc: The widget listens for State updates and rebuilds itself with a <a href="https://bloclibrary.dev/#/flutterbloccoreconcepts?id=blocbuilder">Bloc Widget Builder</a>.</p>
<p>We'll see in a while how the <code>DeviceLoadSuccess</code> State is generated in Bloc.</p>
<p><em>(The code in this article was derived from the excellent <a href="https://bloclibrary.dev/#/flutterweathertutorial">Weather App Tutorial from the Bloc Library</a>)</em></p>
<h1 id="pinetime-data-model" class="section-header"><a href="#pinetime-data-model">3 PineTime Data Model</a></h1>
<p>The <strong>Data Model</strong> is important in Bloc apps... It gets passed to widgets for rendering the user interface. </p>
<p>Let's look at the Data Model for our PineTime Device, as defined in <a href="https://github.com/lupyuen/pinetime-companion/blob/bloc/lib/models/device.dart"><code>lib/models/device.dart</code></a>...</p>
<pre><code class="language-dart">import 'package:equatable/equatable.dart';        //  Object Equality Helper from https://pub.dev/packages/equatable
import 'package:flutter_blue/flutter_blue.dart';  //  Bluetooth LE API from https://github.com/pauldemarco/flutter_blue

/// Data Model for PineTime Device
class Device extends Equatable {
  final BluetoothDevice bluetoothDevice;  //  Bluetooth device for connecting to PineTime, from flutter_blue library
  final String activeFirmwareVersion;     //  Version number of firmware that's running on PineTime (e.g. '1.0.0')
  final String standbyFirmwareVersion;    //  Version number of firmware that's in external flash memory (e.g. '1.1.0')

  /// Constructor for PineTime Device
  const Device({
    this.bluetoothDevice,
    this.activeFirmwareVersion,
    this.standbyFirmwareVersion
  });

  /// Return the properties of PineTime Device
  @override
  List&lt;Object&gt; get props =&gt; [
    bluetoothDevice,
    activeFirmwareVersion,
    standbyFirmwareVersion
  ];
}
</code></pre>
<p>The <code>Device</code> Data Model for PineTime contains two fields <code>activeFirmwareVersion</code> and <code>standbyFirmwareVersion</code>, that store the version numbers of the Active and Standby Firmware on PineTime. </p>
<p>The two fields are rendered by the Device Firmware Widget that we have seen earlier.</p>
<p><em>What's <code>BluetoothDevice</code>?</em></p>
<p>That's the Bluetooth Device returned by the <a href="https://github.com/pauldemarco/flutter_blue"><code>flutter_blue</code> library for Bluetooth LE networking</a>.</p>
<p>In a while we'll see how our Flutter App stores <code>BluetoothDevice</code> into the <code>Device</code> Data Model. And how our app calls <code>BluetoothDevice</code> to send Bluetooth LE requests to PineTime.</p>
<p>To recap, the <code>Device</code> Data Model contains everything we know about PineTime, and provides the means to access PineTime (through <code>BluetoothDevice</code>).</p>
<p><em>Why does <code>Device</code> inherit from the <code>Equatable</code> class?</em></p>
<p><a href="https://pub.dev/packages/equatable"><code>Equatable</code></a> is a helper library that lets us compare two objects for equality.</p>
<p>Two <code>Device</code> Data Models are deemed equivalent if the fields have identical values. This checking for equality is required by Bloc.</p>
<p>Let's find how out <code>BluetoothDevice</code> is used to fetch <code>activeFirmwareVersion</code> and <code>standbyFirmwareVersion</code> from PineTime.</p>
<h1 id="send-bluetooth-le-request-to-pinetime" class="section-header"><a href="#send-bluetooth-le-request-to-pinetime">4 Send Bluetooth LE Request to PineTime</a></h1>
<p>Sending a Bluetooth LE command to PineTime is remarkably simple, straightforward, top to bottom... Thanks to Dart's support for <a href="https://dart.dev/codelabs/async-await"><strong>Asynchronous Programming</strong></a>!</p>
<p>Here are the steps...</p>
<ol>
<li>
<p><strong>Connect to PineTime</strong> over Bluetooth LE</p>
</li>
<li>
<p><strong>Discover the GATT Services</strong> exposed by PineTime</p>
</li>
<li>
<p><strong>Find the right GATT Characteristic</strong> exposed by PineTime</p>
</li>
<li>
<p><strong>Transmit a Write Request</strong> to the GATT Characteristic</p>
</li>
<li>
<p><strong>Receive the response</strong> via a GATT Notification</p>
</li>
<li>
<p><strong>Decode the CBOR</strong> response</p>
</li>
</ol>
<p><img src="https://lupyuen.github.io/images/bloc-bluetooth.png" alt="Sending a Bluetooth LE command to PineTime" /></p>
<h2 id="connect-to-pinetime" class="section-header"><a href="#connect-to-pinetime">4.1 Connect to PineTime</a></h2>
<p>Let's start by connecting to PineTime over Bluetooth LE: <a href="https://github.com/lupyuen/pinetime-companion/blob/bloc/lib/repositories/device_api_client.dart"><code>repositories/device_api_client.dart</code></a></p>
<pre><code class="language-dart">class DeviceApiClient {
  /// Connect to the PineTime device and query the firmare inside
  Future&lt;Device&gt; fetchDevice(BluetoothDevice bluetoothDevice) async {
    //  Connect to PineTime
    await bluetoothDevice.connect();
    ...
</code></pre>
<p><code>DeviceApiClient</code> is the <a href="https://bloclibrary.dev/#/architecture?id=repository"><strong>Data Repository</strong></a> class that we expose to our Flutter App for sending Bluetooth LE commands to PineTime.</p>
<p><em>(Yes the name <code>DeviceApiClient</code> is rather odd... It shall be renamed!)</em></p>
<p><code>fetchDevice()</code> is the method that sends the Bluetooth LE command to query PineTime's firmware images.</p>
<p>The method returns a <code>Device</code> Data Model that contains the Active and Standby Firmware version numbers.</p>
<p><code>bluetoothDevice</code> is the Bluetooth interface returned by the <a href="https://github.com/pauldemarco/flutter_blue"><code>flutter_blue</code> library for Bluetooth LE networking</a>. (More about this later)</p>
<p><em>Why do we use <code>await</code> when connecting to PineTime?</em></p>
<pre><code class="language-dart">  //  Connect to PineTime
  await bluetoothDevice.connect();
</code></pre>
<p>Connecting to PineTime over Bluetooth LE may take a while... And we should wait for it to complete before proceeding.</p>
<p>But we can't let the rest of the app freeze while waiting... What if the human taps the <code>Cancel</code> button!</p>
<p><code>await</code> is exactly what we need. It waits for the <code>connect()</code> method to complete before proceeding to the next step... While keeping the user interface responsive!</p>
<p>That's the beauty of <a href="https://dart.dev/codelabs/async-await"><strong>Asynchronous Programming</strong></a> in Dart... No more Deeply Nested Callbacks and Promises! (Yep React Native gets really messy because of this)</p>
<p><em>Why is the <code>fetchDevice()</code> method declared <code>async</code>?</em></p>
<pre><code class="language-dart">  /// Connect to the PineTime device and query the firmare inside
  Future&lt;Device&gt; fetchDevice(BluetoothDevice bluetoothDevice) async {
</code></pre>
<p>To use <code>await</code> we need two things...</p>
<ol>
<li>
<p>Declare the method as <code>async</code> (like above)</p>
</li>
<li>
<p>Instead of returning a plain <code>Device</code> object, the method now returns a <code>Future&lt;Device&gt;</code> (like above)</p>
</li>
</ol>
<p>Later we'll see that we may simply return a <code>Device</code> object as <code>Future&lt;Device&gt;</code>...</p>
<pre><code class="language-dart">  //  Construct a Device object
  final device = Device( ... );
  //  Return it as Future&lt;Device&gt;
  return device;
</code></pre>
<p>Let's move on to discover GATT Services and Characteristics exposed by PineTime.</p>
<h2 id="discover-gatt-services" class="section-header"><a href="#discover-gatt-services">4.2 Discover GATT Services</a></h2>
<p><a href="https://lupyuen.github.io/pinetime-rust-mynewt/articles/companion">In the previous article</a> we learnt about the <a href="https://github.com/apache/mynewt-mcumgr"><strong>Simple Management Protocol</strong></a> that's exposed by PineTime for querying and updating firmware. We'll be sending the Query Firmware Command to PineTime through this protocol.</p>
<p><em>(Simple Management Protocol is supported today on <a href="https://github.com/apache/mynewt-mcumgr/blob/master/README-mynewt.md"><strong>Mynewt</strong></a> and <a href="https://github.com/apache/mynewt-mcumgr/blob/master/README-zephyr.md"><strong>Zephyr</strong></a> open source embedded operating systems)</em></p>
<p>The Simple Management Protocol is implemented over Bluetooth LE as a <a href="https://learn.adafruit.com/introduction-to-bluetooth-low-energy/gatt">GATT Service</a>. Thus to query the firmware on PineTime, we need to discover the GATT Services exposed by PineTime: <a href="https://github.com/lupyuen/pinetime-companion/blob/bloc/lib/repositories/device_api_client.dart"><code>repositories/device_api_client.dart</code></a></p>
<pre><code class="language-dart">//  Discover the services on PineTime
List&lt;BluetoothService&gt; services = await bluetoothDevice.discoverServices();
</code></pre>
<p><code>discoverServices()</code> talks to PineTine over Bluetooth LE and returns a list of GATT Services exposed by PineTime.</p>
<p>We use <code>await</code> to discover GATT Services, so that the app won't freeze while waiting for the Bluetooth LE response.</p>
<p>The GATT Service for Simple Management Protocol has a UUID (unique ID) of <code>8D53DC1D-1DB7-4CD3-868B-8A527460AA84</code>...</p>
<pre><code class="language-dart">//  Look for Simple Mgmt Protocol Service
for (BluetoothService service in services) {
  if (!listEquals(
    service.uuid.toByteArray(), 
    [0x8d,0x53,0xdc,0x1d,0x1d,0xb7,0x4c,0xd3,0x86,0x8b,0x8a,0x52,0x74,0x60,0xaa,0x84]
  )) { continue; }
</code></pre>
<p>That's how we hunt for the GATT Service.</p>
<h2 id="find-gatt-characteristic" class="section-header"><a href="#find-gatt-characteristic">4.3 Find GATT Characteristic</a></h2>
<p>Next we hunt for the GATT Characteristic (within the GATT Service) for Simple Management Protocol.</p>
<p>To transmit a command to PineTime, we shall write a request message (<a href="https://en.wikipedia.org/wiki/CBOR">in CBOR format</a>) to the GATT Characteristic.</p>
<p>Here's how we find the GATT Characteristic <code>DA2E7828-FBCE-4E01-AE9E-261174997C48</code> for the Simple Management Protocol: <a href="https://github.com/lupyuen/pinetime-companion/blob/bloc/lib/repositories/device_api_client.dart"><code>repositories/device_api_client.dart</code></a></p>
<pre><code class="language-dart">//  Look for Simple Mgmt Protocol Characteristic
var smpCharac;
var characteristics = service.characteristics;
for (BluetoothCharacteristic charac in characteristics) {
  if (!listEquals(
    charac.uuid.toByteArray(),
    [0xda,0x2e,0x78,0x28,0xfb,0xce,0x4e,0x01,0xae,0x9e,0x26,0x11,0x74,0x99,0x7c,0x48]
  )) { continue; }

  //  Found the characteristic
  smpCharac = charac;
  break;
}
</code></pre>
<p>If we can't find the GATT Service or the GATT Characteristic, we throw an exception...</p>
<pre><code class="language-dart">//  If Simple Mgmt Protocol Service or Characteristic not found...
if (smpCharac == null) {
  bluetoothDevice.disconnect();
  throw new Exception('Device doesn\'t support Simple Management Protocol. You may need to flash a suitable firmware.');
}
</code></pre>
<h2 id="write-to-gatt-characteristic" class="section-header"><a href="#write-to-gatt-characteristic">4.4 Write to GATT Characteristic</a></h2>
<p>Now that we have the GATT Characteristic for the Simple Management Protocol, let's talk to the characteristic to send PineTime our Query Firmware Command.</p>
<p>First we compose a request message in CBOR (that includes an 8-byte header): <a href="https://github.com/lupyuen/pinetime-companion/blob/bloc/lib/repositories/device_api_client.dart"><code>repositories/device_api_client.dart</code></a></p>
<pre><code class="language-dart">//  Compose the Query Firmware request (Simple Mgmt Protocol)
final request = composeRequest();
</code></pre>
<p><code>composeRequest()</code> has been documented <a href="https://lupyuen.github.io/pinetime-rust-mynewt/articles/companion">in our previous article</a>.  It sets <code>request</code> to a byte buffer that contains our request message for the Query Firmware Command...</p>

<div class="example-wrap"><pre class="rust rust-example-rendered">
<span class="number">00</span> <span class="number">00</span> <span class="number">00</span> <span class="number">01</span> <span class="number">00</span> <span class="number">01</span> <span class="number">3f</span> <span class="number">00</span> <span class="ident">a0</span></pre></div>
<p><em>(8 bytes for the SMP Message Header, 1 byte for the CBOR Message Body, total 9 bytes)</em></p>
<p>To transmit the request message to PineTime, we write to the GATT Characteristic for the Simple Management Protocol...</p>
<pre><code class="language-dart">//  Transmit the Query Firmware request by writing to the SMP charactertistic
await smpCharac.write(request, withoutResponse: true);
</code></pre>
<p>Again we use <code>await</code> so that the app won't freeze while waiting for the writing to complete.</p>
<p><code>withoutResponse</code> is set to <code>true</code> because we don't expect a synchronous response from the GATT Write operation... Instead we expect the response to be delivered via a GATT Notification. (More about this later)</p>
<p>Yay we have completed 67% of the work needed to send a Bluetooth LE command to PineTime!</p>
<p>Let's move on to receive the Bluetooth LE response from PineTime and decode the response.</p>
<p><img src="https://lupyuen.github.io/images/bloc-bluetooth2.png" alt="Handle Bluetooth LE Response from PineTime" /></p>
<h1 id="handle-bluetooth-le-response-from-pinetime" class="section-header"><a href="#handle-bluetooth-le-response-from-pinetime">5 Handle Bluetooth LE Response from PineTime</a></h1>
<p>Our story thus far...</p>
<ol>
<li>
<p>We have connected to PineTime over Bluetooth LE</p>
</li>
<li>
<p>We have discovered the GATT Service for the Simple Management Protocol</p>
</li>
<li>
<p>We have located the GATT Characteristic for the Simple Management Protocol</p>
</li>
<li>
<p>We have transmitted our Query Firmware Request Message to PineTime... By writing the message to the GATT Characteristic</p>
</li>
</ol>
<p>Now let's get the Query Firmware Response from PineTime in a special way...</p>
<p><em>Will PineTime return the response immediately after writing to the GATT Characteristic?</em></p>
<p><em>Will PineTime let us read the response from the GATT Characteristic?</em></p>
<p>Surprisingly, no and no! </p>
<p>PineTime delivers the response via a <strong>GATT Notification</strong>. (Somewhat similar to Push Notifications on Android and iOS)</p>
<p>The response is <strong>Asynchronous.</strong> Which is probably good for PineTime because it gives PineTime's Firmware more time to prepare and deliver the response. (Remember: PineTime isn't as powerful as a mobile phone)</p>
<p>First we subscribe to GATT Notifications like this: <a href="https://github.com/lupyuen/pinetime-companion/blob/bloc/lib/repositories/device_api_client.dart"><code>repositories/device_api_client.dart</code></a></p>
<pre><code class="language-dart">class DeviceApiClient {
  /// Connect to the PineTime device and query the firmare inside
  Future&lt;Device&gt; fetchDevice(BluetoothDevice bluetoothDevice) async {
    //  Omitted: Transmit the Query Firmware request by writing to the SMP charactertistic
    ...
    //  Subscribe to GATT Notifications from PineTime
    await smpCharac.setNotifyValue(true);
</code></pre>
<p><code>setNotifyValue(true)</code> tells PineTime that we would like to receive GATT Notifications from our GATT Characteristic (the one from Simple Management Protocol).</p>
<pre><code class="language-dart">    //  Create a completer to wait for response from PineTime
    final completer = Completer&lt;typed.Uint8Buffer&gt;();
    //  Create a byte buffer for the response
    final response = typed.Uint8Buffer();
</code></pre>
<p>Next we create a <code>Completer</code> and a byte buffer to hold the response.</p>
<p><em>What's a <code>Completer</code>?</em></p>
<p>It's something that we may <code>await</code> while waiting for our response to be received.</p>
<p><em>(If you're familiar with JavaScript: A <code>Completer</code> is equivalent to a <code>Promise</code>)</em></p>
<p><em>Why can't we just <code>await</code> the response without a <code>Completer</code>?</em></p>
<p>The Bluetooth LE API is somewhat quirky: To receive response bytes we need to use a <strong>Callback Function</strong> like this...</p>
<pre><code class="language-dart">    //  Receive response bytes, chunk by chunk
    smpCharac.value.listen((value) {
      //  Add the chunk to our response buffer
      response.addAll(value);

      //  Get the expected message length
      if (response.length &lt; 4) { return; }           //  Length field not available
      final len = (response[2] &lt;&lt; 8) + response[3];  //  Length field in bytes 2 and 3
      final responseLength = len + 8;  //  Response includes 8 bytes for header

      //  If the received response length is already the expected response length, mark response as complete
      if (response.length &gt;= responseLength &amp;&amp; !completer.isCompleted) {
        completer.complete(response);
      }
    });
</code></pre>
<p>The code in the above Callback Function will be called every time our Flutter App receives a chunk of GATT Notification bytes (<code>value</code>) from PineTime.  We append each chunk to our response buffer.</p>
<p>When we have received the final chunk of response bytes, we call <code>Completer.complete()</code> like this...</p>
<pre><code class="language-dart">    completer.complete(response);
</code></pre>
<p>This signals to <code>await</code> that the response is complete...</p>
<pre><code class="language-dart">    //  Wait for the completer to finish receiving the entire response
    final response2 = await completer.future;
</code></pre>
<p>And that's how <code>await</code> and <code>Completer</code> work together to simplify Callback Functions!</p>
<p><em>(If you're familiar with JavaScript: <code>Completer.complete()</code> is equivalent to <code>Promise.resolve()</code>)</em></p>
<p><em>How will we know when we have received the final chunk of response bytes?</em></p>
<p>Earlier we saw this code for handling the response bytes...</p>
<pre><code class="language-dart">    //  Get the expected message length
    if (response.length &lt; 4) { return; }           //  Length field not available
    final len = (response[2] &lt;&lt; 8) + response[3];  //  Length field in bytes 2 and 3
    final responseLength = len + 8;  //  Response includes 8 bytes for header
</code></pre>
<p>The response message length is stored in bytes 2 and 3 of the response message.</p>
<p>That's how we know when there are no more bytes to be received, and trigger <code>Completer.complete()</code>.</p>
<p><img src="https://lupyuen.github.io/images/bloc-bluetooth3.png" alt="Decode CBOR Response from PineTime" /></p>
<h1 id="decode-cbor-response-from-pinetime" class="section-header"><a href="#decode-cbor-response-from-pinetime">6 Decode CBOR Response from PineTime</a></h1>
<p>Finally we come to the last piece of the puzzle... How <code>BluetoothDevice</code> is used to fetch <code>activeFirmwareVersion</code> and <code>standbyFirmwareVersion</code> from PineTime.</p>
<p>Earlier we have obtained a byte buffer <code>response2</code> that contains the response bytes: 
<a href="https://github.com/lupyuen/pinetime-companion/blob/bloc/lib/repositories/device_api_client.dart"><code>repositories/device_api_client.dart</code></a></p>
<pre><code class="language-dart">class DeviceApiClient {
  /// Connect to the PineTime device and query the firmare inside
  Future&lt;Device&gt; fetchDevice(BluetoothDevice bluetoothDevice) async {
    //  Omitted: Transmit request to PineTime over Bluetooth LE
    ...
    //  Wait for the completer to finish receiving the entire response
    final response2 = await completer.future;
</code></pre>
<p>Before decoding <code>response2</code>, let's disconnect the Bluetooth LE connection to PineTime (and conserve battery power)...</p>
<pre><code class="language-dart">    //  Disconnect the PineTime device
    bluetoothDevice.disconnect();
</code></pre>
<p><em>What's inside <code>response2</code>?</em></p>
<p><code>response2</code> contains...</p>
<ul>
<li>
<p><strong>Response Message Header:</strong> 8 bytes, followed by...</p>
</li>
<li>
<p><strong>Response Message Body:</strong> 244 bytes, encoded in <a href="https://en.wikipedia.org/wiki/CBOR">CBOR</a></p>
</li>
</ul>
<p>It looks like this...</p>

<div class="example-wrap"><pre class="rust rust-example-rendered">
<span class="number">00000000</span>  <span class="number">01</span> <span class="number">00</span> <span class="number">00</span> <span class="ident">f4</span> <span class="number">00</span> <span class="number">01</span> <span class="number">3f</span> <span class="number">00</span>  <span class="ident">bf</span> <span class="number">66</span> <span class="number">69</span> <span class="number">6d</span> <span class="number">61</span> <span class="number">67</span> <span class="number">65</span> <span class="number">73</span>  <span class="op">|</span>.........<span class="ident">fimages</span><span class="op">|</span>
<span class="number">00000010</span>  <span class="number">9f</span> <span class="ident">bf</span> <span class="number">64</span> <span class="number">73</span> <span class="number">6c</span> <span class="number">6f</span> <span class="number">74</span> <span class="number">00</span>  <span class="number">67</span> <span class="number">76</span> <span class="number">65</span> <span class="number">72</span> <span class="number">73</span> <span class="number">69</span> <span class="number">6f</span> <span class="number">6e</span>  <span class="op">|</span>..<span class="ident">dslot</span>.<span class="ident">gversion</span><span class="op">|</span>
<span class="number">00000020</span>  <span class="number">65</span> <span class="number">31</span> <span class="number">2e</span> <span class="number">30</span> <span class="number">2e</span> <span class="number">30</span> <span class="number">64</span> <span class="number">68</span>  <span class="number">61</span> <span class="number">73</span> <span class="number">68</span> <span class="number">58</span> <span class="number">20</span> <span class="ident">ea</span> <span class="ident">bc</span> <span class="number">3a</span>  <span class="op">|</span><span class="ident">e1</span>.<span class="number">0.0dhashX</span> ..:<span class="op">|</span>
<span class="number">00000030</span>  <span class="ident">ce</span> <span class="number">74</span> <span class="ident">a8</span> <span class="number">28</span> <span class="number">4c</span> <span class="number">6f</span> <span class="number">78</span> <span class="ident">c2</span>  <span class="ident">bc</span> <span class="ident">ad</span> <span class="number">3a</span> <span class="ident">e1</span> <span class="number">8d</span> <span class="number">39</span> <span class="number">26</span> <span class="number">75</span>  <span class="op">|</span>.<span class="ident">t</span>.(<span class="ident">Lox</span>...:..<span class="number">9</span><span class="kw-2">&amp;</span><span class="ident">u</span><span class="op">|</span>
<span class="number">00000040</span>  <span class="ident">c7</span> <span class="number">66</span> <span class="ident">c5</span> <span class="number">1f</span> <span class="number">95</span> <span class="number">23</span> <span class="number">0f</span> <span class="number">13</span>  <span class="number">39</span> <span class="number">3f</span> <span class="number">08</span> <span class="number">1c</span> <span class="number">5d</span> <span class="number">68</span> <span class="number">62</span> <span class="number">6f</span>  <span class="op">|</span>.<span class="ident">f</span>...#..<span class="number">9</span><span class="question-mark">?</span>..]<span class="ident">hbo</span><span class="op">|</span>
<span class="number">00000050</span>  <span class="number">6f</span> <span class="number">74</span> <span class="number">61</span> <span class="number">62</span> <span class="number">6c</span> <span class="number">65</span> <span class="ident">f5</span> <span class="number">67</span>  <span class="number">70</span> <span class="number">65</span> <span class="number">6e</span> <span class="number">64</span> <span class="number">69</span> <span class="number">6e</span> <span class="number">67</span> <span class="ident">f4</span>  <span class="op">|</span><span class="ident">otable</span>.<span class="ident">gpending</span>.<span class="op">|</span>
<span class="number">00000060</span>  <span class="number">69</span> <span class="number">63</span> <span class="number">6f</span> <span class="number">6e</span> <span class="number">66</span> <span class="number">69</span> <span class="number">72</span> <span class="number">6d</span>  <span class="number">65</span> <span class="number">64</span> <span class="ident">f5</span> <span class="number">66</span> <span class="number">61</span> <span class="number">63</span> <span class="number">74</span> <span class="number">69</span>  <span class="op">|</span><span class="ident">iconfirmed</span>.<span class="ident">facti</span><span class="op">|</span>
<span class="number">00000070</span>  <span class="number">76</span> <span class="number">65</span> <span class="ident">f5</span> <span class="number">69</span> <span class="number">70</span> <span class="number">65</span> <span class="number">72</span> <span class="number">6d</span>  <span class="number">61</span> <span class="number">6e</span> <span class="number">65</span> <span class="number">6e</span> <span class="number">74</span> <span class="ident">f4</span> <span class="ident">ff</span> <span class="ident">bf</span>  <span class="op">|</span><span class="ident">ve</span>.<span class="ident">ipermanent</span>...<span class="op">|</span>
<span class="number">00000080</span>  <span class="number">64</span> <span class="number">73</span> <span class="number">6c</span> <span class="number">6f</span> <span class="number">74</span> <span class="number">01</span> <span class="number">67</span> <span class="number">76</span>  <span class="number">65</span> <span class="number">72</span> <span class="number">73</span> <span class="number">69</span> <span class="number">6f</span> <span class="number">6e</span> <span class="number">65</span> <span class="number">31</span>  <span class="op">|</span><span class="ident">dslot</span>.<span class="ident">gversione1</span><span class="op">|</span>
<span class="number">00000090</span>  <span class="number">2e</span> <span class="number">31</span> <span class="number">2e</span> <span class="number">30</span> <span class="number">64</span> <span class="number">68</span> <span class="number">61</span> <span class="number">73</span>  <span class="number">68</span> <span class="number">58</span> <span class="number">20</span> <span class="number">0d</span> <span class="number">78</span> <span class="number">49</span> <span class="ident">f7</span> <span class="ident">fe</span>  <span class="op">|</span>.<span class="number">1.0dhashX</span> .<span class="ident">xI</span>..<span class="op">|</span>
<span class="number">000000a0</span>  <span class="number">43</span> <span class="number">92</span> <span class="number">7a</span> <span class="number">87</span> <span class="ident">d7</span> <span class="ident">b4</span> <span class="ident">d5</span> <span class="number">54</span>  <span class="ident">f8</span> <span class="number">43</span> <span class="number">08</span> <span class="number">82</span> <span class="number">33</span> <span class="ident">d8</span> <span class="number">02</span> <span class="ident">d5</span>  <span class="op">|</span><span class="ident">C</span>.<span class="ident">z</span>....<span class="ident">T</span>.<span class="ident">C</span>..<span class="number">3</span>...<span class="op">|</span>
<span class="number">000000b0</span>  <span class="number">09</span> <span class="number">0c</span> <span class="number">20</span> <span class="ident">da</span> <span class="ident">a1</span> <span class="ident">e6</span> <span class="ident">a7</span> <span class="number">77</span>  <span class="number">72</span> <span class="number">99</span> <span class="number">6e</span> <span class="number">68</span> <span class="number">62</span> <span class="number">6f</span> <span class="number">6f</span> <span class="number">74</span>  <span class="op">|</span>.. ....<span class="ident">wr</span>.<span class="ident">nhboot</span><span class="op">|</span>
<span class="number">000000c0</span>  <span class="number">61</span> <span class="number">62</span> <span class="number">6c</span> <span class="number">65</span> <span class="ident">f5</span> <span class="number">67</span> <span class="number">70</span> <span class="number">65</span>  <span class="number">6e</span> <span class="number">64</span> <span class="number">69</span> <span class="number">6e</span> <span class="number">67</span> <span class="ident">f4</span> <span class="number">69</span> <span class="number">63</span>  <span class="op">|</span><span class="ident">able</span>.<span class="ident">gpending</span>.<span class="ident">ic</span><span class="op">|</span>
<span class="number">000000d0</span>  <span class="number">6f</span> <span class="number">6e</span> <span class="number">66</span> <span class="number">69</span> <span class="number">72</span> <span class="number">6d</span> <span class="number">65</span> <span class="number">64</span>  <span class="ident">f4</span> <span class="number">66</span> <span class="number">61</span> <span class="number">63</span> <span class="number">74</span> <span class="number">69</span> <span class="number">76</span> <span class="number">65</span>  <span class="op">|</span><span class="ident">onfirmed</span>.<span class="ident">factive</span><span class="op">|</span>
<span class="number">000000e0</span>  <span class="ident">f4</span> <span class="number">69</span> <span class="number">70</span> <span class="number">65</span> <span class="number">72</span> <span class="number">6d</span> <span class="number">61</span> <span class="number">6e</span>  <span class="number">65</span> <span class="number">6e</span> <span class="number">74</span> <span class="ident">f4</span> <span class="ident">ff</span> <span class="ident">ff</span> <span class="number">6b</span> <span class="number">73</span>  <span class="op">|</span>.<span class="ident">ipermanent</span>...<span class="ident">ks</span><span class="op">|</span>
<span class="number">000000f0</span>  <span class="number">70</span> <span class="number">6c</span> <span class="number">69</span> <span class="number">74</span> <span class="number">53</span> <span class="number">74</span> <span class="number">61</span> <span class="number">74</span>  <span class="number">75</span> <span class="number">73</span> <span class="number">00</span> <span class="ident">ff</span>              <span class="op">|</span><span class="ident">plitStatus</span>..<span class="op">|</span> </pre></div>
<p><em>What's in the Response Message Header?</em></p>
<p>Let's decode the first 8 bytes above according to the Simple Management Protocol message header definition in <a href="https://github.com/apache/mynewt-mcumgr/blob/master/mgmt/include/mgmt/mgmt.h"><code>mynewt-mcumgr/mgmt.h</code></a>...</p>
<table><thead><tr><th align="left">Header Field</th><th align="left">Value</th><th align="left">Description</th></tr></thead><tbody>
<tr><td align="left"><code>Op</code></td><td align="left"><code>01</code></td><td align="left"><a href="https://github.com/apache/mynewt-mcumgr/blob/master/mgmt/include/mgmt/mgmt.h#L33-L37">Operation Code</a> (1 for Read Response)</td></tr>
<tr><td align="left"><code>Flags</code></td><td align="left"><code>00</code></td><td align="left">Unused</td></tr>
<tr><td align="left"><code>Len</code></td><td align="left"><code>00 f4</code></td><td align="left">Length of Message Body (244 bytes)</td></tr>
<tr><td align="left"><code>Group</code></td><td align="left"><code>00 01</code></td><td align="left"><a href="https://github.com/apache/mynewt-mcumgr/blob/master/mgmt/include/mgmt/mgmt.h#L39-L53">Group ID</a> (1 for Image Management)</td></tr>
<tr><td align="left"><code>Seq</code></td><td align="left"><code>3f</code></td><td align="left">Message Sequence Number (should match the request message)</td></tr>
<tr><td align="left"><code>Id</code></td><td align="left"><code>00</code></td><td align="left">Message ID (0 for Image Listing)</td></tr>
</tbody></table>
<p>Yep, this confirms that we have received a response to our Query Firmware (Image Listing) Command.</p>
<p><em>What's all the mumbo jumbo in the Response Message Body?</em></p>
<p>The Response Message Body is encoded in <a href="https://en.wikipedia.org/wiki/CBOR">CBOR</a>, a compact binary form of JSON.</p>
<p>We decode the message body like this...</p>
<pre><code class="language-dart">    //  Extract the CBOR message body
    final body = typed.Uint8Buffer();
    body.addAll(response2.sublist(8));  //  Remove the 8-byte header

    //  Decode the CBOR message body
    final decodedBody = decodeCBOR(body);
</code></pre>
<p><code>decodeCBOR()</code> calls the <a href="https://pub.dev/packages/cbor">CBOR Library</a> to decode the CBOR data into JSON...</p>
<pre><code class="language-dart">/// Decode the CBOR message body
List&lt;dynamic&gt; decodeCBOR(typed.Uint8Buffer payload) {
  //  Get our CBOR instance. Always do this, it correctly initialises the decoder.
  final inst = cbor.Cbor();

  //  Decode from the buffer
  inst.decodeFromBuffer(payload);
  return inst.getDecodedData();
}
</code></pre>
<p>The decoded JSON in <code>decodedBody</code> looks like this...</p>
<pre><code class="language-json">{
    &quot;images&quot;: [
        {
            &quot;slot&quot;: 0,
            &quot;version&quot;: &quot;1.0.0&quot;,
            &quot;hash&quot;: [
                234,188,58,206,116,168,40,76,111,120,194,188,173,58,225,141,57,38,117,199,102,197,31,149,35,15,19,57,63,8,28,93
            ],
            &quot;bootable&quot;: true,
            &quot;pending&quot;: false,
            &quot;confirmed&quot;: true,
            &quot;active&quot;: true,
            &quot;permanent&quot;: false
        },
        {
            &quot;slot&quot;: 1,
            &quot;version&quot;: &quot;1.1.0&quot;,
            &quot;hash&quot;: [
                13,120,73,247,254,67,146,122,135,215,180,213,84,248,67,8,130,51,216,2,213,9,12,32,218,161,230,167,119,114,153,110
            ],
            &quot;bootable&quot;: true,
            &quot;pending&quot;: false,
            &quot;confirmed&quot;: false,
            &quot;active&quot;: false,
            &quot;permanent&quot;: false
        }
    ],
    &quot;splitStatus&quot;: 0
}
</code></pre>
<p><em>Is PineTime telling us what it's hiding?</em></p>
<p>Yes, our PineTime is keeping two firmware images...</p>
<ol>
<li>
<p><strong>Active Firmware:</strong> Version 1.0.0 (Slot 0, in Internal Flash ROM)</p>
</li>
<li>
<p><strong>Standby Firmware:</strong> Version 1.1.0 (Slot 1, in External SPI Flash)</p>
</li>
</ol>
<p>Remember that PineTime always boots from the Active Firmware Image. The Standby Firmware Image is used when updating or rolling back the firmware.</p>
<p>The hash values of the firmware images will be used later when we update the PineTime firmware.</p>
<p><em>How shall we extract the Active and Standby Firmware Versions from the JSON?</em></p>
<p>By accessing the Dynamic List like this...</p>
<pre><code class="language-dart">    //  Get the list of firmware images
    final images = decodedBody[0]['images'] as List&lt;dynamic&gt;;

    //  Construct the Device Data Model with the firmware versions
    final device = Device(
      bluetoothDevice:        bluetoothDevice,
      activeFirmwareVersion:  (images.length &gt;= 1) ? images[0]['version'] : '',
      standbyFirmwareVersion: (images.length &gt;= 2) ? images[1]['version'] : '',
    );

    //  Return the Device Data Model
    return device;
</code></pre>
<p>And that's how we fetch the the Active and Standby Firmware Versions to construct the <code>Device</code> Data Model!</p>
<p><em>Besides fetching the firmware versions, what can else we do with Bluetooth LE commands?</em></p>
<p>Plenty! We may update PineTime's firmware over Bluetooth LE, sync the date and time, push mobile notifications, capture our heart rate and even control smart home gadgets!</p>
<p>PineTime firmware exposes the <a href="https://github.com/apache/mynewt-mcumgr">Simple Management Protocol</a> over Bluetooth LE. The protocol supports a <a href="https://github.com/apache/mynewt-mcumgr/tree/master/cmd">rich set of commands</a> for updating PineTime firmware, accessing the PineTime Flash filesystem, debug logs, runtime statistics, ...</p>
<p>We shall be adding these commands to the PineTime Companion App.</p>
<h1 id="business-logic-states-and-events-in-bloc" class="section-header"><a href="#business-logic-states-and-events-in-bloc">7 Business Logic, States and Events in Bloc</a></h1>
<p>Now back to State Management with Bloc... The right way to manage our complex Flutter App is to use a <strong>Business Logic Class (Bloc),</strong> to drive the <strong>Event Transitions</strong> between the <strong>States</strong> of the app...</p>
<p><img src="https://lupyuen.github.io/images/bloc-transitions1.png" alt="Business Logic driving Event Transitions between the States of the app" /></p>
<p>A shown above, our <strong>Device Bloc</strong> contains the Business Logic that manages three <strong>Device States</strong> (ovals) and two <strong>Device Events</strong> (arrows).</p>
<p>Each Device State corresponds to a screen in our Flutter App...</p>
<ol>
<li>
<p><strong><code>DeviceInitial</code>:</strong> Initial screen of our app</p>
</li>
<li>
<p><strong><code>DeviceLoadInProgress</code>:</strong> PineTime selection screen</p>
</li>
<li>
<p><strong><code>DeviceLoadSuccess</code>:</strong> PineTime summary screen</p>
</li>
</ol>
<p>In Bloc, a Flutter App changes its State (i.e. moves from one screen to the next), when an Event is triggered (like <code>DeviceRequested</code>)</p>
<h2 id="business-logic" class="section-header"><a href="#business-logic">7.1 Business Logic</a></h2>
<p><em>How shall we code the States and Event Transitions in Bloc?</em></p>
<p>With a Bloc Class like so: <a href="https://github.com/lupyuen/pinetime-companion/blob/bloc/lib/blocs/device_bloc.dart"><code>blocs/device_bloc.dart</code></a></p>
<pre><code class="language-dart">/// Device Bloc that manages the Device States and Device Events
class DeviceBloc extends Bloc&lt;DeviceEvent, DeviceState&gt; {
  /// Data Repository that will be used to fetch data from PineTime
  final DeviceRepository deviceRepository;

  /// Construct a Device Bloc. Data Repository is mandatory.
  DeviceBloc({@required this.deviceRepository})
      : assert(deviceRepository != null);

  /// Return the initial Device State (which corresponds to the initial screen)
  @override
  DeviceState get initialState =&gt; DeviceInitial();

  /// When a Device Event is triggered, move to a new Device State (and a new screen)
  @override
  Stream&lt;DeviceState&gt; mapEventToState(DeviceEvent event) async* {
    if (event is DeviceRequested) {
      //  Handle the DeviceRequested Event by loading data from PineTime
      yield* _mapDeviceRequestedToState(event);
    } else if (event is DeviceRefreshRequested) {
      //  Handle the Refresh button by updating the Device Widget
      yield* _mapDeviceRefreshRequestedToState(event);
    }
  }

  /// Handle the DeviceRequested Event by loading data from PineTime
  Stream&lt;DeviceState&gt; _mapDeviceRequestedToState(
    DeviceRequested event,
  ) async* {
    //  Notify the Device Widget that we are loading data
    yield DeviceLoadInProgress();
    try {
      //  Load data from PineTime over Bluetooth LE
      final Device device = await deviceRepository.getDevice(event.device);
      //  Move to the DeviceLoadSuccess State, which renders the Device Summary Widget
      yield DeviceLoadSuccess(device: device);
    } catch (_) {
      //  In case of error, move to the DeviceLoadFailure State
      yield DeviceLoadFailure();
    }
  }
</code></pre>
<p><code>DeviceBloc</code> contains the Business Logic that interprets the triggered Events and drives the States in our app.  Let's inspect the code in <code>DeviceBloc</code>.</p>
<h2 id="trigger-events" class="section-header"><a href="#trigger-events">7.2 Trigger Events</a></h2>
<p><em>How are Events defined in Bloc?</em></p>
<p>We define the <code>DeviceRequested</code> Event like so: <a href="https://github.com/lupyuen/pinetime-companion/blob/bloc/lib/blocs/device_bloc.dart"><code>blocs/device_bloc.dart</code></a></p>
<pre><code class="language-dart">/// Device Requested Event that will shift the Device States
class DeviceRequested extends DeviceEvent {
  /// Bluetooth API for connecting to PineTime, from flutter_blue library
  final BluetoothDevice device;

  /// Construct a Device Requested Event. Bluetooth Device is mandatory.
  const DeviceRequested({@required this.device}) : 
    assert(device != null);

  /// Return the properties of the Device Requested Event
  @override
  List&lt;Object&gt; get props =&gt; [
    device
  ];
}
</code></pre>
<p><em>How are Events triggered in Bloc?</em></p>
<p>Let's look at the first <code>DeviceRequested</code> Event. It moves the app from <code>DeviceInitial</code> State to <code>DeviceLoadInProgress</code> State...</p>
<p><img src="https://lupyuen.github.io/images/bloc-transitions2.png" alt="DeviceRequested Event triggered upon pressing the Search Button" /></p>
<p>The <code>DeviceRequested</code> Event is triggered when the human presses the <strong>Search Button</strong> 🔍 (At top right of the screen)</p>
<p>Here's the code for the Search Button that triggers the <code>DeviceRequested</code> Event: <a href="https://github.com/lupyuen/pinetime-companion/blob/bloc/lib/widgets/device.dart"><code>widgets/device.dart</code></a></p>
<pre><code class="language-dart">/// Implement the Stateful Widget for the PineTime Companion screen
class _DeviceState extends State&lt;Device&gt; {
  /// Render the PineTime Companion screen
  @override
  Widget build(BuildContext context) {
    //  Render the screen with Button Bar above, followed by the Body
    return Scaffold(
      //  Button Bar for the screen
      appBar: AppBar(
        //  Buttons for the Button Bar
        actions: &lt;Widget&gt;[
          ...
          //  Search Button
          IconButton(
            ...
            //  When the Search Button is pressed...
            onPressed: () async {
              //  Navigate to a new screen...
              final device = await Navigator.push(
                context,
                MaterialPageRoute(
                  //  For browsing Bluetooth LE devices
                  builder: (context) =&gt; FindDevice(),
                ),
              );

              //  When the Bluetooth LE browser returns the PineTime Bluetooth Device...
              if (device != null) {
                //  Get the Bloc that handles Device Events...
                BlocProvider
                  .of&lt;DeviceBloc&gt;(context)
                  //  Trigger the DeviceRequested Event...
                  .add(
                    //  With the PineTime Bluetooth Device inside
                    DeviceRequested(
                      device: device
                    )
                  );
              }
</code></pre>
<p><code>FindDevice()</code> shows the widget for browsing Bluetooth LE devices. <code>FindDevice()</code> is defined here: <a href="https://github.com/lupyuen/pinetime-companion/blob/bloc/lib/widgets/find_device.dart"><code>widgets/find_device.dart</code></a></p>
<p><em>What does <code>FindDevice()</code> return?</em></p>
<p>It returns the <code>flutter_blue</code> Bluetooth Device (i.e. PineTime) that was selected by the human.</p>
<p>The Bluetooth Device shall be used in the next step for reading the firmware versions from PineTime.</p>
<h2 id="load-data" class="section-header"><a href="#load-data">7.3 Load Data</a></h2>
<p>Loading data over the web or Bluetooth shouldn't cause our Flutter App to freeze. Bloc has a clever solution: We trigger an Event asynchronously to load data, so that our app remains responsive!</p>
<p>Earlier we have triggered the <code>DeviceRequested</code> Event upon pressing the Search Button: <a href="https://github.com/lupyuen/pinetime-companion/blob/bloc/lib/widgets/device.dart"><code>widgets/device.dart</code></a></p>
<pre><code class="language-dart">//  When Search Button has been pressed and Bluetooth Device has been selected...
//  Get the Bloc that handles Device Events...
BlocProvider
  .of&lt;DeviceBloc&gt;(context)
  //  Trigger the DeviceRequested Event...
  .add(
    //  With the PineTime Bluetooth Device inside
    DeviceRequested(
      device: device
    )
  );
</code></pre>
<p>We handle the <code>DeviceRequested</code> Event in our Device Bloc (Business Logic) like so...</p>
<p><img src="https://lupyuen.github.io/images/bloc-transitions3.png" alt="DeviceRequested Event triggered after loading data from PineTime" /></p>
<p>Here's how we code this in <code>DeviceBloc</code>: <a href="https://github.com/lupyuen/pinetime-companion/blob/bloc/lib/blocs/device_bloc.dart"><code>blocs/device_bloc.dart</code></a></p>
<pre><code class="language-dart">/// Device Bloc that manages the Device States and Device Events
class DeviceBloc extends Bloc&lt;DeviceEvent, DeviceState&gt; {
  ...
  /// When a Device Event is triggered, move to a new Device State (and a new screen)
  @override
  Stream&lt;DeviceState&gt; mapEventToState(DeviceEvent event) async* {
    if (event is DeviceRequested) {
      //  Handle the DeviceRequested Event by loading data from PineTime
      yield* _mapDeviceRequestedToState(event);
      ...
</code></pre>
<p><code>mapEventToState()</code> delegates the handling of the <code>DeviceRequested</code> Event to the function <code>_mapDeviceRequestedToState()</code>, which we'll see in a while.</p>
<p><em>Why is <code>mapEventToState()</code> marked as <code>async*</code> instead of <code>async</code>?</em></p>
<p><code>mapEventToState()</code> responds to an Event by returning one State... Or a delayed sequence of States (which we'll see in <code>_mapDeviceRequestedToState()</code>).</p>
<p>To return a delayed sequence of States, we declare the method as <code>async*</code>.</p>
<p>Also note that instead of returning <code>Future&lt;DeviceState&gt;</code> (a single delayed Device State), we now return <code>Stream&lt;DeviceState&gt;</code> (a delayed sequence of Device States).</p>
<p>And instead of using <code>yield</code>, we use <code>yield*</code> to return a delayed sequence of States.</p>
<p><em>Why does <code>_mapDeviceRequestedToState()</code> return a delayed sequence of States, instead of a single State?</em></p>
<p>Because it returns two States: <code>DeviceLoadInProgress</code> first, followed by <code>DeviceLoadSuccess</code> a short while later.</p>
<p><code>_mapDeviceRequestedToState()</code> responds to the <code>DeviceRequested</code> Event by first returning the <code>DeviceLoadInProgress</code> State: <a href="https://github.com/lupyuen/pinetime-companion/blob/bloc/lib/blocs/device_bloc.dart"><code>blocs/device_bloc.dart</code></a></p>
<pre><code class="language-dart">/// Handle the DeviceRequested Event by loading data from PineTime
Stream&lt;DeviceState&gt; _mapDeviceRequestedToState(
  DeviceRequested event,
) async* {
  //  Notify the Device Widget that we are loading data
  yield DeviceLoadInProgress();
  ...
</code></pre>
<p>When the Device Widget sees the <code>DeviceLoadInProgress</code> State, it renders a Loading Animation to keep the human entertained: <a href="https://github.com/lupyuen/pinetime-companion/blob/bloc/lib/widgets/device.dart"><code>widgets/device.dart</code></a></p>
<pre><code class="language-dart">  //  If Device is loading, show the Loading Animation
  builder: (context, state) {
    if (state is DeviceLoadInProgress) {
      return Center(child: CircularProgressIndicator());
    }
</code></pre>
<p>Next <code>_mapDeviceRequestedToState()</code> performs the actual loading of data: <a href="https://github.com/lupyuen/pinetime-companion/blob/bloc/lib/blocs/device_bloc.dart"><code>blocs/device_bloc.dart</code></a></p>
<pre><code class="language-dart">  //  After notifying the Device Widget that we are loading data...
  //  Load data from PineTime over Bluetooth LE
  final Device device = await deviceRepository.getDevice(event.device);
</code></pre>
<p>It calls the Device Repository to fetch the firmware versions from PineTime over Bluetooth LE.</p>
<p><code>getDevice()</code> (defined in <a href="https://github.com/lupyuen/pinetime-companion/blob/bloc/lib/repositories/device_repository.dart"><code>repositories/device_repository.dart</code></a>) calls <code>fetchDevice()</code>.</p>
<p>We have previously seen <code>fetchDevice()</code> in <a href="https://github.com/lupyuen/pinetime-companion/blob/bloc/lib/repositories/device_api_client.dart"><code>repositories/device_api_client.dart</code></a>... It talks to PineTime over Bluetooth LE to fetch the firmware versions.</p>
<p>Finally <code>_mapDeviceRequestedToState()</code> returns the <code>DeviceLoadSuccess</code> State...</p>
<pre><code class="language-dart">  //  Move to the DeviceLoadSuccess State, which renders the Device Summary Widget
  yield DeviceLoadSuccess(device: device);
</code></pre>
<p>The <code>DeviceLoadSuccess</code> State contains a <code>Device</code> Data Model that has the firmware versions inside.</p>
<p>Asynchronous data loading accomplished!</p>
<h2 id="update-widgets" class="section-header"><a href="#update-widgets">7.4 Update Widgets</a></h2>
<p><em>How are Widgets updated in Bloc?</em></p>
<p>In Bloc, widgets listen for State updates and redraw themselves.</p>
<p>In the previous section we have loaded the <code>Device</code> Data Model from PineTime and updated the State to <code>DeviceLoadSuccess</code>. Now let's listen for State updates and re-render the Device Widget...</p>
<p><img src="https://lupyuen.github.io/images/bloc-transitions4.png" alt="Updating of Widgets" /></p>
<p>The Device Widget listens for the <code>DeviceLoadSuccess</code> State with a <a href="https://bloclibrary.dev/#/flutterbloccoreconcepts?id=blocconsumer"><strong><code>BlocConsumer</code></strong></a> and rebuilds itself like so: <a href="https://github.com/lupyuen/pinetime-companion/blob/bloc/lib/widgets/device.dart"><code>widgets/device.dart</code></a></p>
<pre><code class="language-dart">/// Implement the Stateful Widget for the PineTime Companion screen
class _DeviceState extends State&lt;Device&gt; {
  /// Render the PineTime Companion screen
  @override
  Widget build(BuildContext context) {
    //  Render the screen with Button Bar above, followed by the Body
    return Scaffold(
      //  Button Bar for the screen (omitted)
      appBar: ...,

      //  Body for the screen
      body: 
        ...
        //  Construct a BlocConsumer to listen for updates to the state and rebuild the widget
        BlocConsumer&lt;DeviceBloc, DeviceState&gt;(
          //  Listen for updates to the state (omitted)
          listener: ...

          //  Rebuild the widget when the state is updated
          builder: (context, state) {
            //  When we have loaded the device info...
            if (state is DeviceLoadSuccess) {
              //  Get the device info from the new state
              final device = state.device;

              //  Construct the Device Summary with the device info
              return 
                ...
                DeviceSummary(
                  device: device,
                ),
                ...
</code></pre>
<h2 id="multiple-blocs" class="section-header"><a href="#multiple-blocs">7.5 Multiple Blocs</a></h2>
<p><em>Can we use multiple Blocs?</em></p>
<p>Yes we can! There are two Blocs in our Flutter App...</p>
<ol>
<li>
<p><code>DeviceBloc</code>: For loading data in our app</p>
</li>
<li>
<p><code>ThemeBloc</code>: For updating the UI theme in our app</p>
</li>
</ol>
<p>The Device Widget uses a Theme Bloc nested inside a Device Bloc like this: <a href="https://github.com/lupyuen/pinetime-companion/blob/bloc/lib/widgets/device.dart"><code>widgets/device.dart</code></a></p>
<pre><code class="language-dart">/// Implement the Stateful Widget for the PineTime Companion screen
class _DeviceState extends State&lt;Device&gt; {
  /// Render the PineTime Companion screen
  @override
  Widget build(BuildContext context) {
    //  Render the screen with Button Bar above, followed by the Body
    return Scaffold(
      //  Button Bar for the screen (omitted)
      appBar: ...,

      //  Body for the screen
      body: 
        ...
        //  Construct a Device BlocConsumer to listen for updates to the Device State and rebuild the widget
        BlocConsumer&lt;DeviceBloc, DeviceState&gt;(
          //  Listen for updates to the Device State
          listener: (context, state) {
            //  If the device has been loaded successfully...
            if (state is DeviceLoadSuccess) {
              //  Get the Theme Bloc that handles Theme States...
              BlocProvider
                .of&lt;ThemeBloc&gt;(context)
                //  Trigger a DeviceChanged Event to the Theme Bloc
                .add(
                  DeviceChanged(
                    condition: state.device.condition
                  ),
                  ...
</code></pre>
<p>In the code above, the <code>DeviceLoadSuccess</code> State triggers a <code>DeviceChanged</code> Theme Event (defined in <a href="https://github.com/lupyuen/pinetime-companion/blob/bloc/lib/blocs/theme_bloc.dart"><code>blocs/theme_bloc.dart</code></a>).</p>
<p>We handle the <code>DeviceChanged</code> Theme Event in the Theme Bloc like so: <a href="https://github.com/lupyuen/pinetime-companion/blob/bloc/lib/blocs/theme_bloc.dart"><code>blocs/theme_bloc.dart</code></a></p>
<pre><code class="language-dart">/// Theme Bloc that manages the Theme States and Theme Events
class ThemeBloc extends Bloc&lt;ThemeEvent, ThemeState&gt; {
  /// When a Theme Event is triggered, move to a new Theme State
  @override
  Stream&lt;ThemeState&gt; mapEventToState(ThemeEvent event) async* {
    if (event is DeviceChanged) {
      yield _mapDeviceConditionToTheme(event.condition);
    }
  }

  /// Return the UI Theme based on the current condition
  ThemeState _mapDeviceConditionToTheme(DeviceCondition condition) {
    ThemeState theme;
      //  Return the UI Theme with indigo colours
    theme = ThemeState(
      theme: ThemeData(
        primaryColor: Colors.indigoAccent,
      ),
      color: Colors.indigo,
    );
    return theme;
  }
</code></pre>
<h2 id="tracing-states-and-events" class="section-header"><a href="#tracing-states-and-events">7.6 Tracing States and Events</a></h2>
<p><em>How do we verify that States and Events are working correctly in Bloc?</em></p>
<p>In VSCode, look at the <strong>Debug Console</strong>.</p>
<p>It shows the Events triggered and the transitions between States...</p>

<div class="example-wrap"><pre class="rust rust-example-rendered">
(...<span class="ident">Search</span> <span class="ident">button</span> <span class="ident">pressed</span>...)

<span class="ident">onEvent</span> <span class="ident">DeviceRequested</span>
<span class="ident">Fetching</span> <span class="ident">device</span>...
<span class="ident">onTransition</span> <span class="ident">Transition</span> { 
  <span class="ident">currentState</span>: <span class="ident">DeviceInitial</span>, 
  <span class="ident">event</span>: <span class="ident">DeviceRequested</span>, 
  <span class="ident">nextState</span>: <span class="ident">DeviceLoadInProgress</span> 
}

(...<span class="ident">Transmit</span> <span class="ident">Bluetooth</span> <span class="ident">LE</span> <span class="ident">Request</span> <span class="ident">to</span> <span class="ident">PineTime</span>...)
(...<span class="ident">Receive</span> <span class="ident">Bluetooth</span> <span class="ident">LE</span> <span class="ident">Response</span> <span class="ident">from</span> <span class="ident">PineTime</span>...)

<span class="ident">onTransition</span> <span class="ident">Transition</span> { 
  <span class="ident">currentState</span>: <span class="ident">DeviceLoadInProgress</span>, 
  <span class="ident">event</span>: <span class="ident">DeviceRequested</span>, 
  <span class="ident">nextState</span>: <span class="ident">DeviceLoadSuccess</span> 
}
<span class="ident">onEvent</span> <span class="ident">DeviceChanged</span>

(...<span class="ident">Render</span> <span class="ident">DeviceSummary</span> <span class="ident">widget</span>...)</pre></div>
<p>The messages are generated by the <code>SimpleBlocDelegate</code> class in <a href="https://github.com/lupyuen/pinetime-companion/blob/078e6979341917a0f8247b96d8126bae867e7dee/lib/simple_bloc_delegate.dart"><code>simple_bloc_delegate.dart</code></a></p>
<h1 id="build-and-run-app" class="section-header"><a href="#build-and-run-app">8 Build and Run App</a></h1>
<p>Install VSCode and Flutter SDK according to the instructions here...</p>
<ol>
<li>
<p><a href="https://lupyuen.github.io/pinetime-rust-mynewt/articles/flutter#download-flutter-sdk"><strong>Download Flutter SDK</strong></a></p>
</li>
<li>
<p><a href="https://lupyuen.github.io/pinetime-rust-mynewt/articles/flutter#install-flutter-tools"><strong>Install Flutter Tools</strong></a></p>
</li>
<li>
<p><a href="https://lupyuen.github.io/pinetime-rust-mynewt/articles/flutter#flutter-for-android"><strong>For Android: Install Flutter for Android</strong></a></p>
</li>
<li>
<p><a href="https://lupyuen.github.io/pinetime-rust-mynewt/articles/flutter#flutter-for-ios"><strong>For iOS: Install Flutter for iOS</strong></a></p>
</li>
</ol>
<p>Then proceed to the next section to download and debug the PineTime Companion App.</p>
<h2 id="download-source-code" class="section-header"><a href="#download-source-code">8.1 Download Source Code</a></h2>
<p>The source code for our PineTime Companion App is located here...</p>
<p><a href="https://github.com/lupyuen/pinetime-companion"><code>github.com/lupyuen/pinetime-companion</code></a></p>
<ol>
<li>
<p>In VSCode, click <code>View → Command Palette</code></p>
</li>
<li>
<p>Enter <code>Git Clone</code></p>
</li>
<li>
<p>Enter <code>https://github.com/lupyuen/pinetime-companion</code></p>
</li>
<li>
<p>Select a folder to download the source code</p>
</li>
<li>
<p>When prompted to open the cloned repository, click <code>Open</code></p>
</li>
<li>
<p>When prompted to get missing packages, click <code>Get Packages</code></p>
</li>
</ol>
<h2 id="debug-flutter-app" class="section-header"><a href="#debug-flutter-app">8.2 Debug Flutter App</a></h2>
<p>We're now ready to debug our PineTime Companion App on a real Android or iOS phone!</p>
<ol>
<li>
<p>In VSCode, click <code>Run → Start Debugging</code></p>
<p><img src="https://lupyuen.github.io/images/flutter-debug1.png" alt="Start Debugging in VSCode" /></p>
</li>
<li>
<p>Select the <code>Dart &amp; Flutter</code> debugger</p>
<p><img src="https://lupyuen.github.io/images/flutter-debug2.png" alt="Select Debugger in VSCode" /></p>
</li>
<li>
<p>Wait for the Flutter app to be compiled and deployed to our phone (May take a minute for the first time)</p>
</li>
<li>
<p>For iOS: Check the next section for additional Xcode steps</p>
</li>
<li>
<p>When the Flutter app starts, we'll be able to connect to PineTime to retrieve and display the firmware versions like this...</p>
<ul>
<li>
<p><a href="https://youtu.be/VUYqcD8VGzE">Watch on YouTube</a></p>
</li>
<li>
<p><a href="https://github.com/lupyuen/pinetime-companion/releases/download/v1.0.0/companion-version.mp4">Download the video</a></p>
</li>
</ul>
</li>
</ol>
<p>Check the <strong>Sample Android and iOS Debug Logs</strong> at the end of this article.</p>
<p>VSCode Debugger has many useful features for debugging Flutter apps. Here's what we see when we hit an unhandled exception in our Flutter app...</p>
<p><img src="https://lupyuen.github.io/images/flutter-debug.png" alt="Flutter App with VSCode Debugger" /></p>
<p><a href="https://lupyuen.github.io/images/flutter-debug.png"><em>Larger image</em></a></p>
<ul>
<li>
<p><strong>Dev Tools</strong>: Shows the widgets rendered in our app</p>
</li>
<li>
<p><strong>Variables</strong>: Shows the local and global variables and their values</p>
</li>
<li>
<p><strong>Call Stack</strong>: Function calls leading to the exception or breakpoint</p>
</li>
<li>
<p><strong>Debug Console</strong>: Compilation, deployment and runtime messages</p>
</li>
<li>
<p><strong>Source Code</strong>: Shows the line of code for the exception or breakpoint</p>
</li>
<li>
<p><strong>Debug Toolbar</strong>: Resume execution, step into functions, step over code, hot reload, restart execution... <a href="https://code.visualstudio.com/docs/editor/debugging">More about debugging</a></p>
</li>
</ul>
<p>See this article for more details on building Flutter apps with VSCode, including cool features like Hot Reload...</p>
<ul>
<li><a href="https://flutter.dev/docs/development/tools/vs-code">Flutter Development on VSCode</a></li>
</ul>
<h2 id="sign-flutter-app-for-ios" class="section-header"><a href="#sign-flutter-app-for-ios">8.3 Sign Flutter App for iOS</a></h2>
<p><em>(Skip this section if you're building for Android)</em></p>
<p>This message appears when we debug our iOS app...</p>
<p><img src="https://lupyuen.github.io/images/flutter-sign.png" alt="Xcode Signing" /></p>
<p>Here's what we need to do for iOS...</p>
<ol>
<li>
<p>In VSCode, click <code>Terminal → New Terminal</code></p>
</li>
<li>
<p>At the Terminal prompt, enter...</p>
<pre><code class="language-bash">open ios/Runner.xcworkspace
</code></pre>
<p><img src="https://lupyuen.github.io/images/flutter-workspace.png" alt="Open Xcode workspace" /></p>
</li>
<li>
<p>In Xcode, click <code>Runner → Targets Runner → Signing &amp; Capabilities</code></p>
<p><img src="https://lupyuen.github.io/images/flutter-sign2.png" alt="Xcode Signing" /></p>
</li>
<li>
<p>Set <code>Team</code> to our Apple Developer Account</p>
</li>
<li>
<p>Set <code>Bundle Identifier</code> to a unique name</p>
</li>
<li>
<p>On our iPhone, click <code>Settings → General → Device Management</code></p>
</li>
<li>
<p>Set the Trust Settings like this...</p>
<p><img src="https://lupyuen.github.io/images/flutter-trust.png" alt="Trust iOS Developer" /></p>
</li>
</ol>
<p>We should be able to launch and debug our Flutter app using the instructions from the previous section.</p>
<p>Check the <strong>Sample iOS Debug Log</strong> at the end of this article.</p>
<p><img src="https://lupyuen.github.io/images/bloc-ios.jpg" alt="PineTime Companion App on iPhone" /></p>
<p><em>PineTime Companion App on iPhone</em></p>
<h1 id="whats-next" class="section-header"><a href="#whats-next">9 What's Next</a></h1>
<p>The code in this article is part of the upcoming open source <a href="https://github.com/lupyuen/pinetime-companion"><strong>PineTime Companion App</strong></a> for Android and iOS. So that we can update the firmware on our PineTime Smart Watches wirelessly, sync the date and time, show notifications from our phone, chart our heart rate, ... Maybe even control our smart home gadgets! </p>
<p>We'll be doing lots more coding...</p>
<ol>
<li>
<p><strong>Handle Other PineTime Commands:</strong> Update firmware, sync date and time, show mobile notifications, control smart home gadgets (via IFTTT and MQTT), ...</p>
<p>We shall do this by taking the <a href="https://github.com/apache/mynewt-newtmgr">Newt Manager</a> code in Go and converting it to Flutter and Dart, as explained here...</p>
<p><em><a href="https://lupyuen.github.io/pinetime-rust-mynewt/articles/companion">&quot;Convert Go to Flutter and Dart for PineTime Companion App&quot;</a></em></p>
</li>
<li>
<p><strong>Companion App for Linux Phones (like PinePhone):</strong> We shall take the <a href="https://github.com/apache/mynewt-newtmgr">Newt Manager</a> code in Go and wrap it into a GTK3 app, using the <a href="https://github.com/gotk3/gotk3"><code>gotk3</code> library</a>...</p>
<p><em><a href="https://lupyuen.github.io/pinetime-rust-mynewt/articles/gotk3">&quot;Your First GTK App with Go and VSCodium&quot;</a></em></p>
</li>
<li>
<p><strong>PineTime Firmware Support:</strong> Today our PineTime Companion App talks to Mynewt and Zephyr operating systems on PineTime. We hope to implement the same Bluetooth LE protocol (Simple Management Protocol) on other operating systems, so that they may also enjoy wireless firmware updates...</p>
<p><em><a href="https://lupyuen.github.io/pinetime-rust-mynewt/articles/dfu">&quot;Firmware Update over Bluetooth Low Energy on PineTime Smart Watch&quot;</a></em></p>
</li>
</ol>
<p><em>Why are we maintaining two code bases: Flutter (for Android and iOS) and Go (for Linux phones)?</em></p>
<p>Because Flutter is probably the best way to build mobile apps... But it's not officially supported for Linux phones.  The <code>flutter_blue</code> plugin doesn't support Linux either.</p>
<p>So we need to stick with Go for Linux phones.</p>
<p>We're now exploring <a href="https://github.com/go-flutter-desktop/go-flutter"><strong><code>go-flutter</code></strong></a> for porting the Flutter App to Linux. And recode <code>flutter_blue</code> via FFI to a Linux Bluetooth LE library (in Go or C).</p>
<p><em>(Maybe someday when Flutter is officially supported on Linux phones, we can scrap the Go version!)</em></p>
<p>If you're keen to help out with the PineTime Companion App (or anything else in PineTime), <strong>come chat with the PineTime FOSS Community (and me) in the PineTime Chatroom!</strong></p>
<p><a href="https://wiki.pine64.org/index.php/PineTime#Community">PineTime Chatroom on Matrix / Discord / Telegram / IRC</a></p>
<h1 id="further-reading" class="section-header"><a href="#further-reading">10 Further Reading</a></h1>
<p><em><a href="https://lupyuen.github.io/pinetime-rust-mynewt/articles/companion">&quot;Convert Go to Flutter and Dart for PineTime Companion App&quot;</a></em></p>
<p><em><a href="https://lupyuen.github.io/pinetime-rust-mynewt/articles/flutter">&quot;Your First Bluetooth Low Energy App with Flutter&quot;</a></em></p>
<p><em><a href="https://lupyuen.github.io/pinetime-rust-mynewt/articles/gotk3">&quot;Your First GTK App with Go and VSCodium&quot;</a></em></p>
<p><em><a href="https://lupyuen.github.io/pinetime-rust-mynewt/articles/mcuboot">&quot;MCUBoot Bootloader for PineTime Smart Watch (nRF52)&quot;</a></em></p>
<p><em><a href="https://lupyuen.github.io/pinetime-rust-mynewt/articles/dfu">&quot;Firmware Update over Bluetooth Low Energy on PineTime Smart Watch&quot;</a></em></p>
<p><em><a href="https://lupyuen.github.io/pinetime-rust-mynewt/articles/dfutest">&quot;Wireless Firmware Update In Action on PineTime Smart Watch (nRF52)&quot;</a></em></p>
<p><a href="https://github.com/lupyuen/pinetime-rust-mynewt/blob/master/README.md">Check out the other PineTime articles</a></p>
<p><a href="https://lupyuen.github.io/rss.xml">RSS Feed</a></p>
<h1 id="sample-android-debug-log" class="section-header"><a href="#sample-android-debug-log">11 Sample Android Debug Log</a></h1>
<pre><code>Launching lib/main.dart on Pixel 4 XL in debug mode...
✓ Built build/app/outputs/apk/debug/app-debug.apk.
I/FlutterBluePlugin(20366): setup
Connecting to VM Service at ws://127.0.0.1:56153/XI6AjAwoNUM=/ws
W/er_blue_exampl(20366): Accessing hidden method Lsun/misc/Unsafe;-&gt;getLong(Ljava/lang/Object;J)J (greylist,core-platform-api, linking, allowed)
W/er_blue_exampl(20366): Accessing hidden method Lsun/misc/Unsafe;-&gt;arrayBaseOffset(Ljava/lang/Class;)I (greylist,core-platform-api, linking, allowed)
W/er_blue_exampl(20366): Accessing hidden method Lsun/misc/Unsafe;-&gt;copyMemory(JJJ)V (greylist, linking, allowed)
W/er_blue_exampl(20366): Accessing hidden method Lsun/misc/Unsafe;-&gt;objectFieldOffset(Ljava/lang/reflect/Field;)J (greylist,core-platform-api, linking, allowed)
W/er_blue_exampl(20366): Accessing hidden method Lsun/misc/Unsafe;-&gt;getByte(J)B (greylist,core-platform-api, linking, allowed)
W/er_blue_exampl(20366): Accessing hidden method Lsun/misc/Unsafe;-&gt;getByte(Ljava/lang/Object;J)B (greylist,core-platform-api, linking, allowed)
W/er_blue_exampl(20366): Accessing hidden method Lsun/misc/Unsafe;-&gt;getLong(J)J (greylist,core-platform-api, linking, allowed)
W/er_blue_exampl(20366): Accessing hidden method Lsun/misc/Unsafe;-&gt;putByte(JB)V (greylist,core-platform-api, linking, allowed)
W/er_blue_exampl(20366): Accessing hidden method Lsun/misc/Unsafe;-&gt;putByte(Ljava/lang/Object;JB)V (greylist,core-platform-api, linking, allowed)
W/er_blue_exampl(20366): Accessing hidden method Lsun/misc/Unsafe;-&gt;getLong(Ljava/lang/Object;J)J (greylist,core-platform-api, reflection, allowed)
W/er_blue_exampl(20366): Accessing hidden method Lsun/misc/Unsafe;-&gt;getLong(Ljava/lang/Object;J)J (greylist,core-platform-api, reflection, allowed)
W/er_blue_exampl(20366): Accessing hidden field Ljava/nio/Buffer;-&gt;address:J (greylist, reflection, allowed)
D/FlutterBluePlugin(20366): mDevices size: 0
D/FlutterBluePlugin(20366): mDevices size: 0
D/BluetoothAdapter(20366): isLeEnabled(): ON
D/BluetoothLeScanner(20366): onScannerRegistered() - status=0 scannerId=8 mScannerId=0
D/FlutterBluePlugin(20366): mDevices size: 0
I/flutter (20366): onEvent DeviceRequested
I/flutter (20366): Fetching device...
I/flutter (20366): onTransition Transition { currentState: DeviceInitial, event: DeviceRequested, nextState: DeviceLoadInProgress }
D/BluetoothGatt(20366): connect() - device: E8:C1:1A:12:BA:89, auto: true
D/BluetoothGatt(20366): registerApp()
D/BluetoothGatt(20366): registerApp() - UUID=e0c4eada-3709-4f5c-80c4-2d39b4cc0309
D/BluetoothGatt(20366): onClientRegistered() - status=0 clientIf=9
D/FlutterBluePlugin(20366): mDevices size: 1
D/BluetoothGatt(20366): onClientConnectionState() - status=0 clientIf=9 device=E8:C1:1A:12:BA:89
D/FlutterBluePlugin(20366): [onConnectionStateChange] status: 0 newState: 2
I/flutter (20366): Device: BluetoothDevice{id: E8:C1:1A:12:BA:89, name: pinetime, type: BluetoothDeviceType.le, isDiscoveringServices: false, _services: []
D/BluetoothGatt(20366): discoverServices() - device: E8:C1:1A:12:BA:89
D/BluetoothAdapter(20366): isLeEnabled(): ON
D/BluetoothGatt(20366): onConnectionUpdated() - Device=E8:C1:1A:12:BA:89 interval=6 latency=0 timeout=500 status=0
D/BluetoothGatt(20366): onSearchComplete() = Device=E8:C1:1A:12:BA:89 Status=0
D/FlutterBluePlugin(20366): [onServicesDiscovered] count: 6 status: 0
D/BluetoothGatt(20366): setCharacteristicNotification() - uuid: da2e7828-fbce-4e01-ae9e-261174997c48 enable: true
D/FlutterBluePlugin(20366): [onDescriptorWrite] uuid: 00002902-0000-1000-8000-00805f9b34fb status: 0
I/flutter (20366): Encoded {NmpBase:{hdr:{Op:0 Flags:0 Len:0 Group:1 Seq:63 Id:0}}} {} to:
I/flutter (20366): a0
I/flutter (20366): Encoded:
I/flutter (20366): 00 00 00 01 00 01 3f 00 a0
I/flutter (20366): Notify:
D/BluetoothGatt(20366): onConnectionUpdated() - Device=E8:C1:1A:12:BA:89 interval=36 latency=0 timeout=500 status=0
D/FlutterBluePlugin(20366): [onCharacteristicWrite] uuid: da2e7828-fbce-4e01-ae9e-261174997c48 status: 0
D/FlutterBluePlugin(20366): [onCharacteristicChanged] uuid: da2e7828-fbce-4e01-ae9e-261174997c48
I/chatty  (20366): uid=10302(com.pauldemarco.flutter_blue_example) Binder:20366_2 identical 2 lines
D/FlutterBluePlugin(20366): [onCharacteristicChanged] uuid: da2e7828-fbce-4e01-ae9e-261174997c48
I/flutter (20366): Notify: 01 00 00 f4 00 01 3f 00 bf 66 69 6d 61 67 65 73 9f bf 64 73
I/flutter (20366): Notify: 6c 6f 74 00 67 76 65 72 73 69 6f 6e 65 31 2e 30 2e 30 64 68
I/flutter (20366): Notify: 61 73 68 58 20 ea bc 3a ce 74 a8 28 4c 6f 78 c2 bc ad 3a e1
I/flutter (20366): Notify: 8d 39 26 75 c7 66 c5 1f 95 23 0f 13 39 3f 08 1c 5d 68 62 6f
D/FlutterBluePlugin(20366): [onCharacteristicChanged] uuid: da2e7828-fbce-4e01-ae9e-261174997c48
I/flutter (20366): Notify: 6f 74 61 62 6c 65 f5 67 70 65 6e 64 69 6e 67 f4 69 63 6f 6e
D/FlutterBluePlugin(20366): [onCharacteristicChanged] uuid: da2e7828-fbce-4e01-ae9e-261174997c48
I/chatty  (20366): uid=10302(com.pauldemarco.flutter_blue_example) Binder:20366_2 identical 2 lines
D/FlutterBluePlugin(20366): [onCharacteristicChanged] uuid: da2e7828-fbce-4e01-ae9e-261174997c48
I/flutter (20366): Notify: 66 69 72 6d 65 64 f5 66 61 63 74 69 76 65 f5 69 70 65 72 6d
I/flutter (20366): Notify: 61 6e 65 6e 74 f4 ff bf 64 73 6c 6f 74 01 67 76 65 72 73 69
I/flutter (20366): Notify: 6f 6e 65 31 2e 31 2e 30 64 68 61 73 68 58 20 0d 78 49 f7 fe
I/flutter (20366): Notify: 43 92 7a 87 d7 b4 d5 54 f8 43 08 82 33 d8 02 d5 09 0c 20 da
D/FlutterBluePlugin(20366): [onCharacteristicChanged] uuid: da2e7828-fbce-4e01-ae9e-261174997c48
I/chatty  (20366): uid=10302(com.pauldemarco.flutter_blue_example) Binder:20366_2 identical 1 line
D/FlutterBluePlugin(20366): [onCharacteristicChanged] uuid: da2e7828-fbce-4e01-ae9e-261174997c48
I/flutter (20366): Notify: a1 e6 a7 77 72 99 6e 68 62 6f 6f 74 61 62 6c 65 f5 67 70 65
I/flutter (20366): Notify: 6e 64 69 6e 67 f4 69 63 6f 6e 66 69 72 6d 65 64 f4 66 61 63
I/flutter (20366): Notify: 74 69 76 65 f4 69 70 65 72 6d 61 6e 65 6e 74 f4 ff ff 6b 73
D/FlutterBluePlugin(20366): [onCharacteristicChanged] uuid: da2e7828-fbce-4e01-ae9e-261174997c48
I/flutter (20366): Notify: 70 6c 69 74 53 74 61 74 75 73 00 ff
I/flutter (20366): Response Length: 252 vs 252
D/BluetoothGatt(20366): cancelOpen() - device: E8:C1:1A:12:BA:89
D/BluetoothGatt(20366): onClientConnectionState() - status=0 clientIf=9 device=E8:C1:1A:12:BA:89
D/FlutterBluePlugin(20366): [onConnectionStateChange] status: 0 newState: 0
D/BluetoothGatt(20366): close()
D/BluetoothGatt(20366): unregisterApp() - mClientIf=9
I/flutter (20366): Decoded CBOR:
I/flutter (20366): Entry 0   : Value is =&gt; {images: [{slot: 0, version: 1.0.0, hash: [234, 188, 58, 206, 116, 168, 40, 76, 111, 120, 194, 188, 173, 58, 225, 141, 57, 38, 117, 199, 102, 197, 31, 149, 35, 15, 19, 57, 63, 8, 28, 93], bootable: true, pending: false, confirmed: true, active: true, permanent: false}, {slot: 1, version: 1.1.0, hash: [13, 120, 73, 247, 254, 67, 146, 122, 135, 215, 180, 213, 84, 248, 67, 8, 130, 51, 216, 2, 213, 9, 12, 32, 218, 161, 230, 167, 119, 114, 153, 110], bootable: true, pending: false, confirmed: false, active: false, permanent: false}], splitStatus: 0}
I/flutter (20366): {&quot;images&quot;:[{&quot;slot&quot;:0,&quot;version&quot;:&quot;1.0.0&quot;,&quot;hash&quot;:[234,188,58,206,116,168,40,76,111,120,194,188,173,58,225,141,57,38,117,199,102,197,31,149,35,15,19,57,63,8,28,93],&quot;bootable&quot;:true,&quot;pending&quot;:false,&quot;confirmed&quot;:true,&quot;active&quot;:true,&quot;permanent&quot;:false},{&quot;slot&quot;:1,&quot;version&quot;:&quot;1.1.0&quot;,&quot;hash&quot;:[13,120,73,247,254,67,146,122,135,215,180,213,84,248,67,8,130,51,216,2,213,9,12,32,218,161,230,167,119,114,153,110],&quot;bootable&quot;:true,&quot;pending&quot;:false,&quot;confirmed&quot;:false,&quot;active&quot;:false,&quot;permanent&quot;:false}],&quot;splitStatus&quot;:0}
I/flutter (20366): Decoded Response: [{images: [{slot: 0, version: 1.0.0, hash: [234, 188, 58, 206, 116, 168, 40, 76, 111, 120, 194, 188, 173, 58, 225, 141, 57, 38, 117, 199, 102, 197, 31, 149, 35, 15, 19, 57, 63, 8, 28, 93], bootable: true, pending: false, confirmed: true, active: true, permanent: false}, {slot: 1, version: 1.1.0, hash: [13, 120, 73, 247, 254, 67, 146, 122, 135, 215, 180, 213, 84, 248, 67, 8, 130, 51, 216, 2, 213, 9, 12, 32, 218, 161, 230, 167, 119, 114, 153, 110], bootable: true, pending: false, confirmed: false, active: false, permanent: false}], splitStatus: 0}]
I/flutter (20366): onTransition Transition { currentState: DeviceLoadInProgress, event: DeviceRequested, nextState: DeviceLoadSuccess }
I/flutter (20366): onEvent DeviceChanged
I/flutter (20366): onTransition Transition { currentState: ThemeState, event: DeviceChanged, nextState: ThemeState }
Application finished.
Exited (sigterm)</code></pre><h1 id="sample-ios-debug-log" class="section-header"><a href="#sample-ios-debug-log">12 Sample iOS Debug Log</a></h1>
<div class="example-wrap"><pre class="rust rust-example-rendered">
<span class="ident">Launching</span> <span class="ident">lib</span><span class="op">/</span><span class="ident">main</span>.<span class="ident">dart</span> <span class="ident">on</span> <span class="ident">iPhone</span> <span class="number">6</span> <span class="ident">Plus</span> <span class="kw">in</span> <span class="ident">debug</span> <span class="ident">mode</span>...
<span class="ident">Warning</span>: <span class="ident">Missing</span> <span class="ident">build</span> <span class="ident">name</span> (<span class="ident">CFBundleShortVersionString</span>).
<span class="ident">Warning</span>: <span class="ident">Missing</span> <span class="ident">build</span> <span class="ident">number</span> (<span class="ident">CFBundleVersion</span>).
<span class="ident">Action</span> <span class="ident">Required</span>: <span class="ident">You</span> <span class="ident">must</span> <span class="ident">set</span> <span class="ident">a</span> <span class="ident">build</span> <span class="ident">name</span> <span class="ident">and</span> <span class="ident">number</span> <span class="kw">in</span> <span class="ident">the</span> <span class="ident">pubspec</span>.<span class="ident">yaml</span> <span class="ident">file</span> <span class="ident">version</span> <span class="ident">field</span> <span class="ident">before</span> <span class="ident">submitting</span> <span class="ident">to</span> <span class="ident">the</span> <span class="ident">App</span> <span class="ident">Store</span>.
<span class="ident">Automatically</span> <span class="ident">signing</span> <span class="ident">iOS</span> <span class="kw">for</span> <span class="ident">device</span> <span class="ident">deployment</span> <span class="ident">using</span> <span class="ident">specified</span> <span class="ident">development</span> <span class="ident">team</span> <span class="kw">in</span> <span class="ident">Xcode</span> <span class="ident">project</span>:
<span class="ident">Running</span> <span class="ident">pod</span> <span class="ident">install</span>...                                              <span class="number">3.0s</span>
<span class="ident">Xcode</span> <span class="ident">build</span> <span class="ident">done</span>.                                           <span class="number">131.8s</span>
<span class="ident">Installing</span> <span class="ident">and</span> <span class="ident">launching</span>...                                        <span class="number">24.7s</span>
<span class="ident">Connecting</span> <span class="ident">to</span> <span class="ident">VM</span> <span class="ident">Service</span> <span class="ident">at</span> <span class="ident">ws</span>:<span class="comment">//localhost:1024/ws</span>
<span class="ident">getConnectedDevices</span> <span class="ident">periphs</span> <span class="ident">size</span>: <span class="number">0</span>
<span class="ident">getConnectedDevices</span> <span class="ident">periphs</span> <span class="ident">size</span>: <span class="number">0</span>
<span class="ident">flutter</span>: <span class="ident">onEvent</span> <span class="ident">DeviceRequested</span>
<span class="ident">flutter</span>: <span class="ident">Fetching</span> <span class="ident">device</span>...
<span class="ident">flutter</span>: <span class="ident">onTransition</span> <span class="ident">Transition</span> { <span class="ident">currentState</span>: <span class="ident">DeviceInitial</span>, <span class="ident">event</span>: <span class="ident">DeviceRequested</span>, <span class="ident">nextState</span>: <span class="ident">DeviceLoadInProgress</span> }
<span class="ident">didConnectPeripheral</span>
<span class="ident">flutter</span>: <span class="ident">Device</span>: <span class="ident">BluetoothDevice</span>{<span class="ident">id</span>: <span class="ident">CED8A589</span><span class="op">-</span><span class="number">7B65</span><span class="op">-</span><span class="number">508A</span><span class="op">-</span><span class="ident">F6B6</span><span class="op">-</span><span class="ident">F0B3EFE3DFFC</span>, <span class="ident">name</span>: <span class="ident">pinetime</span>, <span class="kw">type</span>: <span class="ident">BluetoothDeviceType</span>.<span class="ident">le</span>, <span class="ident">isDiscoveringServices</span>: <span class="bool-val">false</span>, <span class="ident">_services</span>: []
<span class="ident">didDiscoverServices</span>
<span class="ident">Found</span> <span class="ident">service</span>: <span class="number">1811</span>
<span class="ident">Found</span> <span class="ident">service</span>: <span class="number">180A</span>
<span class="ident">Found</span> <span class="ident">service</span>: <span class="number">8D53DC1D</span><span class="op">-</span><span class="number">1DB7</span><span class="op">-</span><span class="number">4CD3</span><span class="op">-</span><span class="number">868B</span><span class="op">-</span><span class="number">8A527460AA84</span>
<span class="ident">Found</span> <span class="ident">service</span>: <span class="number">59462F12</span><span class="op">-</span><span class="number">9543</span><span class="op">-</span><span class="number">9999</span><span class="op">-</span><span class="number">12C8</span><span class="op">-</span><span class="number">58B459A2712D</span>
<span class="ident">didDiscoverCharacteristicsForService</span>
<span class="ident">didDiscoverCharacteristicsForService</span>
<span class="ident">didDiscoverCharacteristicsForService</span>
<span class="ident">didDiscoverCharacteristicsForService</span>
<span class="ident">didDiscoverDescriptorsForCharacteristic</span>
<span class="ident">didDiscoverDescriptorsForCharacteristic</span>
<span class="ident">didDiscoverDescriptorsForCharacteristic</span>
<span class="ident">didDiscoverDescriptorsForCharacteristic</span>
<span class="ident">didDiscoverDescriptorsForCharacteristic</span>
<span class="ident">didDiscoverDescriptorsForCharacteristic</span>
<span class="ident">didDiscoverDescriptorsForCharacteristic</span>
<span class="ident">didDiscoverDescriptorsForCharacteristic</span>
<span class="ident">didDiscoverDescriptorsForCharacteristic</span>
<span class="ident">didDiscoverDescriptorsForCharacteristic</span>
<span class="ident">peripheral</span> <span class="ident">uuid</span>:<span class="ident">CED8A589</span><span class="op">-</span><span class="number">7B65</span><span class="op">-</span><span class="number">508A</span><span class="op">-</span><span class="ident">F6B6</span><span class="op">-</span><span class="ident">F0B3EFE3DFFC</span>
<span class="ident">service</span> <span class="ident">uuid</span>:<span class="number">00001811</span><span class="op">-</span><span class="number">0000</span><span class="op">-</span><span class="number">1000</span><span class="op">-</span><span class="number">8000</span><span class="op">-</span><span class="number">00805f9b34fb</span>
<span class="ident">uuid</span>: <span class="number">00002a47</span><span class="op">-</span><span class="number">0000</span><span class="op">-</span><span class="number">1000</span><span class="op">-</span><span class="number">8000</span><span class="op">-</span><span class="number">00805f9b34fb</span> <span class="ident">value</span>: (<span class="ident">null</span>)
<span class="ident">uuid</span>: <span class="number">00002a46</span><span class="op">-</span><span class="number">0000</span><span class="op">-</span><span class="number">1000</span><span class="op">-</span><span class="number">8000</span><span class="op">-</span><span class="number">00805f9b34fb</span> <span class="ident">value</span>: (<span class="ident">null</span>)
<span class="ident">uuid</span>: <span class="number">00002a48</span><span class="op">-</span><span class="number">0000</span><span class="op">-</span><span class="number">1000</span><span class="op">-</span><span class="number">8000</span><span class="op">-</span><span class="number">00805f9b34fb</span> <span class="ident">value</span>: (<span class="ident">null</span>)
<span class="ident">uuid</span>: <span class="number">00002a45</span><span class="op">-</span><span class="number">0000</span><span class="op">-</span><span class="number">1000</span><span class="op">-</span><span class="number">8000</span><span class="op">-</span><span class="number">00805f9b34fb</span> <span class="ident">value</span>: (<span class="ident">null</span>)
<span class="ident">uuid</span>: <span class="number">00002a44</span><span class="op">-</span><span class="number">0000</span><span class="op">-</span><span class="number">1000</span><span class="op">-</span><span class="number">8000</span><span class="op">-</span><span class="number">00805f9b34fb</span> <span class="ident">value</span>: (<span class="ident">null</span>)
<span class="ident">peripheral</span> <span class="ident">uuid</span>:<span class="ident">CED8A589</span><span class="op">-</span><span class="number">7B65</span><span class="op">-</span><span class="number">508A</span><span class="op">-</span><span class="ident">F6B6</span><span class="op">-</span><span class="ident">F0B3EFE3DFFC</span>
<span class="ident">service</span> <span class="ident">uuid</span>:<span class="number">0000180a</span><span class="op">-</span><span class="number">0000</span><span class="op">-</span><span class="number">1000</span><span class="op">-</span><span class="number">8000</span><span class="op">-</span><span class="number">00805f9b34fb</span>
<span class="ident">uuid</span>: <span class="number">00002a24</span><span class="op">-</span><span class="number">0000</span><span class="op">-</span><span class="number">1000</span><span class="op">-</span><span class="number">8000</span><span class="op">-</span><span class="number">00805f9b34fb</span> <span class="ident">value</span>: (<span class="ident">null</span>)
<span class="ident">uuid</span>: <span class="number">00002a26</span><span class="op">-</span><span class="number">0000</span><span class="op">-</span><span class="number">1000</span><span class="op">-</span><span class="number">8000</span><span class="op">-</span><span class="number">00805f9b34fb</span> <span class="ident">value</span>: (<span class="ident">null</span>)
<span class="ident">peripheral</span> <span class="ident">uuid</span>:<span class="ident">CED8A589</span><span class="op">-</span><span class="number">7B65</span><span class="op">-</span><span class="number">508A</span><span class="op">-</span><span class="ident">F6B6</span><span class="op">-</span><span class="ident">F0B3EFE3DFFC</span>
<span class="ident">service</span> <span class="ident">uuid</span>:<span class="number">8d53dc1d</span><span class="op">-</span><span class="number">1db7</span><span class="op">-</span><span class="number">4cd3</span><span class="op">-</span><span class="number">868b</span><span class="op">-</span><span class="number">8a527460aa84</span>
<span class="ident">uuid</span>: <span class="ident">da2e7828</span><span class="op">-</span><span class="ident">fbce</span><span class="op">-</span><span class="number">4e01</span><span class="op">-</span><span class="ident">ae9e</span><span class="op">-</span><span class="number">261174997c48</span> <span class="ident">value</span>: (<span class="ident">null</span>)
<span class="ident">peripheral</span> <span class="ident">uuid</span>:<span class="ident">CED8A589</span><span class="op">-</span><span class="number">7B65</span><span class="op">-</span><span class="number">508A</span><span class="op">-</span><span class="ident">F6B6</span><span class="op">-</span><span class="ident">F0B3EFE3DFFC</span>
<span class="ident">service</span> <span class="ident">uuid</span>:<span class="number">59462f12</span><span class="op">-</span><span class="number">9543</span><span class="op">-</span><span class="number">9999</span><span class="op">-</span><span class="number">12c8</span><span class="op">-</span><span class="number">58b459a2712d</span>
<span class="ident">uuid</span>: <span class="number">5c3a659e</span><span class="op">-</span><span class="number">897e-45e1</span><span class="op">-</span><span class="ident">b016</span><span class="op">-</span><span class="number">007107c96df6</span> <span class="ident">value</span>: (<span class="ident">null</span>)
<span class="ident">uuid</span>: <span class="number">5c3a659e</span><span class="op">-</span><span class="number">897e-45e1</span><span class="op">-</span><span class="ident">b016</span><span class="op">-</span><span class="number">007107c96df7</span> <span class="ident">value</span>: (<span class="ident">null</span>)
<span class="ident">didUpdateNotificationStateForCharacteristic</span>
<span class="ident">uuid</span>: <span class="ident">da2e7828</span><span class="op">-</span><span class="ident">fbce</span><span class="op">-</span><span class="number">4e01</span><span class="op">-</span><span class="ident">ae9e</span><span class="op">-</span><span class="number">261174997c48</span> <span class="ident">value</span>: (<span class="ident">null</span>)
<span class="ident">flutter</span>: <span class="ident">Encoded</span> {<span class="ident">NmpBase</span>:{<span class="ident">hdr</span>:{<span class="ident">Op</span>:<span class="number">0</span> <span class="ident">Flags</span>:<span class="number">0</span> <span class="ident">Len</span>:<span class="number">0</span> <span class="ident">Group</span>:<span class="number">1</span> <span class="ident">Seq</span>:<span class="number">18</span> <span class="ident">Id</span>:<span class="number">0</span>}}} {} <span class="ident">to</span>:
<span class="ident">a0</span>
<span class="ident">flutter</span>: <span class="ident">Encoded</span>:
<span class="number">00</span> <span class="number">00</span> <span class="number">00</span> <span class="number">01</span> <span class="number">00</span> <span class="number">01</span> <span class="number">12</span> <span class="number">00</span> <span class="ident">a0</span>
<span class="ident">flutter</span>: <span class="ident">Notify</span>:
<span class="ident">didUpdateValueForCharacteristic</span> <span class="ident">CED8A589</span><span class="op">-</span><span class="number">7B65</span><span class="op">-</span><span class="number">508A</span><span class="op">-</span><span class="ident">F6B6</span><span class="op">-</span><span class="ident">F0B3EFE3DFFC</span>
<span class="ident">uuid</span>: <span class="ident">da2e7828</span><span class="op">-</span><span class="ident">fbce</span><span class="op">-</span><span class="number">4e01</span><span class="op">-</span><span class="ident">ae9e</span><span class="op">-</span><span class="number">261174997c48</span> <span class="ident">value</span>: <span class="op">&lt;</span><span class="number">010000f4</span> <span class="number">00011200</span> <span class="ident">bf66696d</span> <span class="number">61676573</span> <span class="number">9fbf6473</span> <span class="number">6c6f7400</span> <span class="number">67766572</span> <span class="number">73696f6e</span> <span class="number">65312e30</span> <span class="number">2e306468</span> <span class="number">61736858</span> <span class="number">20eabc3a</span> <span class="ident">ce74a828</span> <span class="number">4c6f78c2</span> <span class="ident">bcad3ae1</span> <span class="number">8d392675</span> <span class="ident">c766c51f</span> <span class="number">95230f13</span> <span class="number">393f081c</span> <span class="number">5d68626f</span> <span class="number">6f746162</span> <span class="number">6c65f567</span> <span class="number">70656e64</span> <span class="number">696e67f4</span> <span class="number">69636f6e</span> <span class="number">6669726d</span> <span class="number">6564f566</span> <span class="number">61637469</span> <span class="number">7665f569</span> <span class="number">7065726d</span> <span class="number">616e656e</span> <span class="number">74f4ffbf</span> <span class="number">64736c6f</span> <span class="number">74016776</span> <span class="number">65727369</span> <span class="number">6f6e6531</span> <span class="number">2e312e30</span> <span class="number">64686173</span> <span class="number">6858200d</span> <span class="number">7849f7fe</span> <span class="number">43927a87</span> <span class="ident">d7b4d554</span> <span class="ident">f8430882</span> <span class="number">33d802d5</span> <span class="number">090c20da</span> <span class="ident">a1e6</span><span class="op">&gt;</span>
<span class="ident">uuid</span>: <span class="ident">da2e7828</span><span class="op">-</span><span class="ident">fbce</span><span class="op">-</span><span class="number">4e01</span><span class="op">-</span><span class="ident">ae9e</span><span class="op">-</span><span class="number">261174997c48</span> <span class="ident">value</span>: <span class="op">&lt;</span><span class="number">010000f4</span> <span class="number">00011200</span> <span class="ident">bf66696d</span> <span class="number">61676573</span> <span class="number">9fbf6473</span> <span class="number">6c6f7400</span> <span class="number">67766572</span> <span class="number">73696f6e</span> <span class="number">65312e30</span> <span class="number">2e306468</span> <span class="number">61736858</span> <span class="number">20eabc3a</span> <span class="ident">ce74a828</span> <span class="number">4c6f78c2</span> <span class="ident">bcad3ae1</span> <span class="number">8d392675</span> <span class="ident">c766c51f</span> <span class="number">95230f13</span> <span class="number">393f081c</span> <span class="number">5d68626f</span> <span class="number">6f746162</span> <span class="number">6c65f567</span> <span class="number">70656e64</span> <span class="number">696e67f4</span> <span class="number">69636f6e</span> <span class="number">6669726d</span> <span class="number">6564f566</span> <span class="number">61637469</span> <span class="number">7665f569</span> <span class="number">7065726d</span> <span class="number">616e656e</span> <span class="number">74f4ffbf</span> <span class="number">64736c6f</span> <span class="number">74016776</span> <span class="number">65727369</span> <span class="number">6f6e6531</span> <span class="number">2e312e30</span> <span class="number">64686173</span> <span class="number">6858200d</span> <span class="number">7849f7fe</span> <span class="number">43927a87</span> <span class="ident">d7b4d554</span> <span class="ident">f8430882</span> <span class="number">33d802d5</span> <span class="number">090c20da</span> <span class="ident">a1e6</span><span class="op">&gt;</span>
<span class="ident">didUpdateValueForCharacteristic</span> <span class="ident">CED8A589</span><span class="op">-</span><span class="number">7B65</span><span class="op">-</span><span class="number">508A</span><span class="op">-</span><span class="ident">F6B6</span><span class="op">-</span><span class="ident">F0B3EFE3DFFC</span>
<span class="ident">uuid</span>: <span class="ident">da2e7828</span><span class="op">-</span><span class="ident">fbce</span><span class="op">-</span><span class="number">4e01</span><span class="op">-</span><span class="ident">ae9e</span><span class="op">-</span><span class="number">261174997c48</span> <span class="ident">value</span>: <span class="op">&lt;</span><span class="ident">a7777299</span> <span class="number">6e68626f</span> <span class="number">6f746162</span> <span class="number">6c65f567</span> <span class="number">70656e64</span> <span class="number">696e67f4</span> <span class="number">69636f6e</span> <span class="number">6669726d</span> <span class="number">6564f466</span> <span class="number">61637469</span> <span class="number">7665f469</span> <span class="number">7065726d</span> <span class="number">616e656e</span> <span class="number">74f4ffff</span> <span class="number">6b73706c</span> <span class="number">69745374</span> <span class="number">61747573</span> <span class="number">00ff</span><span class="op">&gt;</span>
<span class="ident">uuid</span>: <span class="ident">da2e7828</span><span class="op">-</span><span class="ident">fbce</span><span class="op">-</span><span class="number">4e01</span><span class="op">-</span><span class="ident">ae9e</span><span class="op">-</span><span class="number">261174997c48</span> <span class="ident">value</span>: <span class="op">&lt;</span><span class="ident">a7777299</span> <span class="number">6e68626f</span> <span class="number">6f746162</span> <span class="number">6c65f567</span> <span class="number">70656e64</span> <span class="number">696e67f4</span> <span class="number">69636f6e</span> <span class="number">6669726d</span> <span class="number">6564f466</span> <span class="number">61637469</span> <span class="number">7665f469</span> <span class="number">7065726d</span> <span class="number">616e656e</span> <span class="number">74f4ffff</span> <span class="number">6b73706c</span> <span class="number">69745374</span> <span class="number">61747573</span> <span class="number">00ff</span><span class="op">&gt;</span>
<span class="ident">flutter</span>: <span class="ident">Notify</span>: <span class="number">01</span> <span class="number">00</span> <span class="number">00</span> <span class="ident">f4</span> <span class="number">00</span> <span class="number">01</span> <span class="number">12</span> <span class="number">00</span> <span class="ident">bf</span> <span class="number">66</span> <span class="number">69</span> <span class="number">6d</span> <span class="number">61</span> <span class="number">67</span> <span class="number">65</span> <span class="number">73</span> <span class="number">9f</span> <span class="ident">bf</span> <span class="number">64</span> <span class="number">73</span> <span class="number">6c</span> <span class="number">6f</span> <span class="number">74</span> <span class="number">00</span> <span class="number">67</span> <span class="number">76</span> <span class="number">65</span> <span class="number">72</span> <span class="number">73</span> <span class="number">69</span> <span class="number">6f</span> <span class="number">6e</span> <span class="number">65</span> <span class="number">31</span> <span class="number">2e</span> <span class="number">30</span> <span class="number">2e</span> <span class="number">30</span> <span class="number">64</span> <span class="number">68</span> <span class="number">61</span> <span class="number">73</span> <span class="number">68</span> <span class="number">58</span> <span class="number">20</span> <span class="ident">ea</span> <span class="ident">bc</span> <span class="number">3a</span> <span class="ident">ce</span> <span class="number">74</span> <span class="ident">a8</span> <span class="number">28</span> <span class="number">4c</span> <span class="number">6f</span> <span class="number">78</span> <span class="ident">c2</span> <span class="ident">bc</span> <span class="ident">ad</span> <span class="number">3a</span> <span class="ident">e1</span> <span class="number">8d</span> <span class="number">39</span> <span class="number">26</span> <span class="number">75</span> <span class="ident">c7</span> <span class="number">66</span> <span class="ident">c5</span> <span class="number">1f</span> <span class="number">95</span> <span class="number">23</span> <span class="number">0f</span> <span class="number">13</span> <span class="number">39</span> <span class="number">3f</span> <span class="number">08</span> <span class="number">1c</span> <span class="number">5d</span> <span class="number">68</span> <span class="number">62</span> <span class="number">6f</span> <span class="number">6f</span> <span class="number">74</span> <span class="number">61</span> <span class="number">62</span> <span class="number">6c</span> <span class="number">65</span> <span class="ident">f5</span> <span class="number">67</span> <span class="number">70</span> <span class="number">65</span> <span class="number">6e</span> <span class="number">64</span> <span class="number">69</span> <span class="number">6e</span> <span class="number">67</span> <span class="ident">f4</span> <span class="number">69</span> <span class="number">63</span> <span class="number">6f</span> <span class="number">6e</span> <span class="number">66</span> <span class="number">69</span> <span class="number">72</span> <span class="number">6d</span> <span class="number">65</span> <span class="number">64</span> <span class="ident">f5</span> <span class="number">66</span> <span class="number">61</span> <span class="number">63</span> <span class="number">74</span> <span class="number">69</span> <span class="number">76</span> <span class="number">65</span> <span class="ident">f5</span> <span class="number">69</span> <span class="number">70</span> <span class="number">65</span> <span class="number">72</span> <span class="number">6d</span> <span class="number">61</span> <span class="number">6e</span> <span class="number">65</span> <span class="number">6e</span> <span class="number">74</span> <span class="ident">f4</span> <span class="ident">ff</span> <span class="ident">bf</span> <span class="number">64</span> <span class="number">73</span> <span class="number">6c</span> <span class="number">6f</span> <span class="number">74</span> <span class="number">01</span> <span class="number">67</span> <span class="number">76</span> <span class="number">65</span> <span class="number">72</span> <span class="number">73</span> <span class="number">69</span> <span class="number">6f</span> <span class="number">6e</span> <span class="number">65</span> <span class="number">31</span> <span class="number">2e</span> <span class="number">31</span> <span class="number">2e</span> <span class="number">30</span> <span class="number">64</span> <span class="number">68</span> <span class="number">61</span> <span class="number">73</span> <span class="number">68</span> <span class="number">58</span> <span class="number">20</span> <span class="number">0d</span> <span class="number">78</span> <span class="number">49</span> <span class="ident">f7</span> <span class="ident">fe</span> <span class="number">43</span> <span class="number">92</span> <span class="number">7a</span> <span class="number">87</span> <span class="ident">d7</span> <span class="ident">b4</span> <span class="ident">d5</span> <span class="number">54</span> <span class="ident">f8</span> <span class="number">43</span> <span class="number">08</span> <span class="number">82</span> <span class="number">33</span> <span class="ident">d8</span> <span class="number">02</span> <span class="ident">d5</span> <span class="number">09</span> <span class="number">0c</span> <span class="number">20</span> <span class="ident">da</span> <span class="ident">a1</span> <span class="ident">e6</span>
<span class="ident">flutter</span>: <span class="ident">Notify</span>: <span class="ident">a7</span> <span class="number">77</span> <span class="number">72</span> <span class="number">99</span> <span class="number">6e</span> <span class="number">68</span> <span class="number">62</span> <span class="number">6f</span> <span class="number">6f</span> <span class="number">74</span> <span class="number">61</span> <span class="number">62</span> <span class="number">6c</span> <span class="number">65</span> <span class="ident">f5</span> <span class="number">67</span> <span class="number">70</span> <span class="number">65</span> <span class="number">6e</span> <span class="number">64</span> <span class="number">69</span> <span class="number">6e</span> <span class="number">67</span> <span class="ident">f4</span> <span class="number">69</span> <span class="number">63</span> <span class="number">6f</span> <span class="number">6e</span> <span class="number">66</span> <span class="number">69</span> <span class="number">72</span> <span class="number">6d</span> <span class="number">65</span> <span class="number">64</span> <span class="ident">f4</span> <span class="number">66</span> <span class="number">61</span> <span class="number">63</span> <span class="number">74</span> <span class="number">69</span> <span class="number">76</span> <span class="number">65</span> <span class="ident">f4</span> <span class="number">69</span> <span class="number">70</span> <span class="number">65</span> <span class="number">72</span> <span class="number">6d</span> <span class="number">61</span> <span class="number">6e</span> <span class="number">65</span> <span class="number">6e</span> <span class="number">74</span> <span class="ident">f4</span> <span class="ident">ff</span> <span class="ident">ff</span> <span class="number">6b</span> <span class="number">73</span> <span class="number">70</span> <span class="number">6c</span> <span class="number">69</span> <span class="number">74</span> <span class="number">53</span> <span class="number">74</span> <span class="number">61</span> <span class="number">74</span> <span class="number">75</span> <span class="number">73</span> <span class="number">00</span> <span class="ident">ff</span>
<span class="ident">flutter</span>: <span class="ident">Response</span> <span class="ident">Length</span>: <span class="number">252</span> <span class="ident">vs</span> <span class="number">252</span>
<span class="ident">didDisconnectPeripheral</span>
<span class="ident">flutter</span>: <span class="ident">Decoded</span> <span class="ident">CBOR</span>:
<span class="ident">Entry</span> <span class="number">0</span>   : <span class="ident">Value</span> <span class="ident">is</span> <span class="op">=</span><span class="op">&gt;</span> {<span class="ident">images</span>: [{<span class="ident">slot</span>: <span class="number">0</span>, <span class="ident">version</span>: <span class="number">1.0</span>.<span class="number">0</span>, <span class="ident">hash</span>: [<span class="number">234</span>, <span class="number">188</span>, <span class="number">58</span>, <span class="number">206</span>, <span class="number">116</span>, <span class="number">168</span>, <span class="number">40</span>, <span class="number">76</span>, <span class="number">111</span>, <span class="number">120</span>, <span class="number">194</span>, <span class="number">188</span>, <span class="number">173</span>, <span class="number">58</span>, <span class="number">225</span>, <span class="number">141</span>, <span class="number">57</span>, <span class="number">38</span>, <span class="number">117</span>, <span class="number">199</span>, <span class="number">102</span>, <span class="number">197</span>, <span class="number">31</span>, <span class="number">149</span>, <span class="number">35</span>, <span class="number">15</span>, <span class="number">19</span>, <span class="number">57</span>, <span class="number">63</span>, <span class="number">8</span>, <span class="number">28</span>, <span class="number">93</span>], <span class="ident">bootable</span>: <span class="bool-val">true</span>, <span class="ident">pending</span>: <span class="bool-val">false</span>, <span class="ident">confirmed</span>: <span class="bool-val">true</span>, <span class="ident">active</span>: <span class="bool-val">true</span>, <span class="ident">permanent</span>: <span class="bool-val">false</span>}, {<span class="ident">slot</span>: <span class="number">1</span>, <span class="ident">version</span>: <span class="number">1.1</span>.<span class="number">0</span>, <span class="ident">hash</span>: [<span class="number">13</span>, <span class="number">120</span>, <span class="number">73</span>, <span class="number">247</span>, <span class="number">254</span>, <span class="number">67</span>, <span class="number">146</span>, <span class="number">122</span>, <span class="number">135</span>, <span class="number">215</span>, <span class="number">180</span>, <span class="number">213</span>, <span class="number">84</span>, <span class="number">248</span>, <span class="number">67</span>, <span class="number">8</span>, <span class="number">130</span>, <span class="number">51</span>, <span class="number">216</span>, <span class="number">2</span>, <span class="number">213</span>, <span class="number">9</span>, <span class="number">12</span>, <span class="number">32</span>, <span class="number">218</span>, <span class="number">161</span>, <span class="number">230</span>, <span class="number">167</span>, <span class="number">119</span>, <span class="number">114</span>, <span class="number">153</span>, <span class="number">110</span>], <span class="ident">bootable</span>: <span class="bool-val">true</span>, <span class="ident">pending</span>: <span class="bool-val">false</span>, <span class="ident">confirmed</span>: <span class="bool-val">false</span>, <span class="ident">active</span>: <span class="bool-val">false</span>, <span class="ident">permanent</span>: <span class="bool-val">false</span>}], <span class="ident">splitStatus</span>: <span class="number">0</span>}
<span class="ident">flutter</span>: {<span class="string">&quot;images&quot;</span>:[{<span class="string">&quot;slot&quot;</span>:<span class="number">0</span>,<span class="string">&quot;version&quot;</span>:<span class="string">&quot;1.0.0&quot;</span>,<span class="string">&quot;hash&quot;</span>:[<span class="number">234</span>,<span class="number">188</span>,<span class="number">58</span>,<span class="number">206</span>,<span class="number">116</span>,<span class="number">168</span>,<span class="number">40</span>,<span class="number">76</span>,<span class="number">111</span>,<span class="number">120</span>,<span class="number">194</span>,<span class="number">188</span>,<span class="number">173</span>,<span class="number">58</span>,<span class="number">225</span>,<span class="number">141</span>,<span class="number">57</span>,<span class="number">38</span>,<span class="number">117</span>,<span class="number">199</span>,<span class="number">102</span>,<span class="number">197</span>,<span class="number">31</span>,<span class="number">149</span>,<span class="number">35</span>,<span class="number">15</span>,<span class="number">19</span>,<span class="number">57</span>,<span class="number">63</span>,<span class="number">8</span>,<span class="number">28</span>,<span class="number">93</span>],<span class="string">&quot;bootable&quot;</span>:<span class="bool-val">true</span>,<span class="string">&quot;pending&quot;</span>:<span class="bool-val">false</span>,<span class="string">&quot;confirmed&quot;</span>:<span class="bool-val">true</span>,<span class="string">&quot;active&quot;</span>:<span class="bool-val">true</span>,<span class="string">&quot;permanent&quot;</span>:<span class="bool-val">false</span>},{<span class="string">&quot;slot&quot;</span>:<span class="number">1</span>,<span class="string">&quot;version&quot;</span>:<span class="string">&quot;1.1.0&quot;</span>,<span class="string">&quot;hash&quot;</span>:[<span class="number">13</span>,<span class="number">120</span>,<span class="number">73</span>,<span class="number">247</span>,<span class="number">254</span>,<span class="number">67</span>,<span class="number">146</span>,<span class="number">122</span>,<span class="number">135</span>,<span class="number">215</span>,<span class="number">180</span>,<span class="number">213</span>,<span class="number">84</span>,<span class="number">248</span>,<span class="number">67</span>,<span class="number">8</span>,<span class="number">130</span>,<span class="number">51</span>,<span class="number">216</span>,<span class="number">2</span>,<span class="number">213</span>,<span class="number">9</span>,<span class="number">12</span>,<span class="number">32</span>,<span class="number">218</span>,<span class="number">161</span>,<span class="number">230</span>,<span class="number">167</span>,<span class="number">119</span>,<span class="number">114</span>,<span class="number">153</span>,<span class="number">110</span>],<span class="string">&quot;bootable&quot;</span>:<span class="bool-val">true</span>,<span class="string">&quot;pending&quot;</span>:<span class="bool-val">false</span>,<span class="string">&quot;confirmed&quot;</span>:<span class="bool-val">false</span>,<span class="string">&quot;active&quot;</span>:<span class="bool-val">false</span>,<span class="string">&quot;permanent&quot;</span>:<span class="bool-val">false</span>}],<span class="string">&quot;splitStatus&quot;</span>:<span class="number">0</span>}
<span class="ident">flutter</span>: <span class="ident">Decoded</span> <span class="ident">Response</span>: [{<span class="ident">images</span>: [{<span class="ident">slot</span>: <span class="number">0</span>, <span class="ident">version</span>: <span class="number">1.0</span>.<span class="number">0</span>, <span class="ident">hash</span>: [<span class="number">234</span>, <span class="number">188</span>, <span class="number">58</span>, <span class="number">206</span>, <span class="number">116</span>, <span class="number">168</span>, <span class="number">40</span>, <span class="number">76</span>, <span class="number">111</span>, <span class="number">120</span>, <span class="number">194</span>, <span class="number">188</span>, <span class="number">173</span>, <span class="number">58</span>, <span class="number">225</span>, <span class="number">141</span>, <span class="number">57</span>, <span class="number">38</span>, <span class="number">117</span>, <span class="number">199</span>, <span class="number">102</span>, <span class="number">197</span>, <span class="number">31</span>, <span class="number">149</span>, <span class="number">35</span>, <span class="number">15</span>, <span class="number">19</span>, <span class="number">57</span>, <span class="number">63</span>, <span class="number">8</span>, <span class="number">28</span>, <span class="number">93</span>], <span class="ident">bootable</span>: <span class="bool-val">true</span>, <span class="ident">pending</span>: <span class="bool-val">false</span>, <span class="ident">confirmed</span>: <span class="bool-val">true</span>, <span class="ident">active</span>: <span class="bool-val">true</span>, <span class="ident">permanent</span>: <span class="bool-val">false</span>}, {<span class="ident">slot</span>: <span class="number">1</span>, <span class="ident">version</span>: <span class="number">1.1</span>.<span class="number">0</span>, <span class="ident">hash</span>: [<span class="number">13</span>, <span class="number">120</span>, <span class="number">73</span>, <span class="number">247</span>, <span class="number">254</span>, <span class="number">67</span>, <span class="number">146</span>, <span class="number">122</span>, <span class="number">135</span>, <span class="number">215</span>, <span class="number">180</span>, <span class="number">213</span>, <span class="number">84</span>, <span class="number">248</span>, <span class="number">67</span>, <span class="number">8</span>, <span class="number">130</span>, <span class="number">51</span>, <span class="number">216</span>, <span class="number">2</span>, <span class="number">213</span>, <span class="number">9</span>, <span class="number">12</span>, <span class="number">32</span>, <span class="number">218</span>, <span class="number">161</span>, <span class="number">230</span>, <span class="number">167</span>, <span class="number">119</span>, <span class="number">114</span>, <span class="number">153</span>, <span class="number">110</span>], <span class="ident">bootable</span>: <span class="bool-val">true</span>, <span class="ident">pending</span>: <span class="bool-val">false</span>, <span class="ident">confirmed</span>: <span class="bool-val">false</span>, <span class="ident">active</span>: <span class="bool-val">false</span>, <span class="ident">permanent</span>: <span class="bool-val">false</span>}], <span class="ident">splitStatus</span>: <span class="number">0</span>}]
<span class="ident">flutter</span>: <span class="ident">onTransition</span> <span class="ident">Transition</span> { <span class="ident">currentState</span>: <span class="ident">DeviceLoadInProgress</span>, <span class="ident">event</span>: <span class="ident">DeviceRequested</span>, <span class="ident">nextState</span>: <span class="ident">DeviceLoadSuccess</span> }
<span class="ident">flutter</span>: <span class="ident">onEvent</span> <span class="ident">DeviceChanged</span>
<span class="ident">flutter</span>: <span class="ident">onTransition</span> <span class="ident">Transition</span> { <span class="ident">currentState</span>: <span class="ident">ThemeState</span>, <span class="ident">event</span>: <span class="ident">DeviceChanged</span>, <span class="ident">nextState</span>: <span class="ident">ThemeState</span> }
<span class="ident">Lost</span> <span class="ident">connection</span> <span class="ident">to</span> <span class="ident">device</span>.
<span class="ident">Exited</span> (<span class="ident">sigterm</span>)</pre></div>

    
</body>
</html>